From 8a2ce8192e525ca784da43175d5be48bfb2ca0ec Mon Sep 17 00:00:00 2001 From: rhambach Date: Mon, 23 May 2016 12:06:02 +0200 Subject: [PATCH 01/23] first working version of arraytrace passing numpy-arrays directly --- pyzdde/arraytrace.py | 142 +++++++++++++++++++++++++-- pyzdde/arraytrace/arrayTraceClient.c | 64 +++++++++++- pyzdde/arraytrace/arrayTraceClient.h | 6 ++ pyzdde/arraytrace/makefile.win | 101 +++++++++++++++++++ pyzdde/zdde.py | 8 +- 5 files changed, 305 insertions(+), 16 deletions(-) create mode 100644 pyzdde/arraytrace/makefile.win diff --git a/pyzdde/arraytrace.py b/pyzdde/arraytrace.py index 079585c..364c0c4 100644 --- a/pyzdde/arraytrace.py +++ b/pyzdde/arraytrace.py @@ -28,6 +28,8 @@ import sys as _sys import ctypes as _ct import collections as _co +import numpy as _np +from numpy.ctypeslib import ndpointer as NPTR #import gc as _gc if _sys.version_info[0] > 2: @@ -49,16 +51,22 @@ def _is64bit(): """ return _sys.maxsize > 2**31 - 1 -_dllDir = "arraytrace\\x64\\Release\\" if _is64bit() else "arraytrace\\Release\\" +_dllDir = "arraytrace\\x64\\Release\\" if _is64bit() else "arraytrace\\win32\\Release\\" _dllName = "ArrayTrace.dll" _dllpath = _os.path.join(_os.path.dirname(_os.path.realpath(__file__)), _dllDir) # load the arrayTrace library _array_trace_lib = _ct.WinDLL(_dllpath + _dllName) +# shorthands for Types +_INT = _ct.c_int; +_INT1D = _np.ctypeslib.ndpointer(ndim=1,dtype=_np.int,flags=["C_CONTIGUOUS","ALIGNED"]) +_INT2D = _np.ctypeslib.ndpointer(ndim=2,dtype=_np.int,flags=["C_CONTIGUOUS","ALIGNED"]) +_DBL1D = _np.ctypeslib.ndpointer(ndim=1,dtype=_np.double,flags=["C_CONTIGUOUS","ALIGNED"]) +_DBL2D = _np.ctypeslib.ndpointer(ndim=2,dtype=_np.double,flags=["C_CONTIGUOUS","ALIGNED"]) +# int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) _arrayTrace = _array_trace_lib.arrayTrace -# specify argtypes and restype -_arrayTrace.restype = _ct.c_int -_arrayTrace.argtypes = [_ct.POINTER(DdeArrayData), _ct.c_uint] - +_arrayTrace.restype = _INT +_arrayTrace.argtypes = [_ct.POINTER(DdeArrayData), _INT] + def zArrayTrace(rd, timeout=5000): """function to trace large number of rays on lens file in the LDE of main @@ -153,6 +161,40 @@ def getRayDataArray(numRays, tType=0, mode=0, startSurf=None, endSurf=-1, setattr(rd[0], k, kwargs[k]) return rd + +def zGetTraceArray_new(field, pupil, waveNum=None, intensity=None, + mode=0, surf=-1, want_opd=0, timeout=5000): + + # handle input arguments + assert 2 == field.ndim == pupil.ndim, 'field and pupil should be 2d arrays' + assert field.shape == pupil.shape, 'we expect field and pupil points for each ray' + nRays = field.shape[0]; + if waveNum is None: waveNum=1; + if _np.isscalar(waveNum): waveNum=_np.zeros(nRays,dtype=_np.int)+waveNum; + if intensity is None: intensity=1; + if _np.isscalar(intensity): intensity=_np.zeros(nRays)+intensity; + + # set up output arguments + error=_np.zeros(nRays,dtype=_np.int); + vigcode=_np.zeros(nRays,dtype=_np.int); + pos=_np.zeros((nRays,3)); + dir=_np.zeros((nRays,3)); + normal=_np.zeros((nRays,3)); + opd=_np.zeros(nRays); + + # arrayGetTrace(int nrays, double field[][2], double pupil[][2], + # double intensity[], int wave_num[], int mode, int surf, int want_opd, + # int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], + # double opd[], unsigned int timeout); + _arrayGetTrace = _array_trace_lib.arrayGetTrace + _arrayGetTrace.restype = _INT + _arrayGetTrace.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT,_INT, + _INT1D,_INT1D,_DBL2D,_DBL2D,_DBL2D,_DBL1D,_ct.c_uint] + _arrayGetTrace(nRays,field,pupil,intensity,waveNum,mode,surf,want_opd, + error,vigcode,pos,dir,normal,opd,timeout) + + return (error,vigcode,pos,dir,normal,opd,intensity); + def zGetTraceArray(numRays, hx=None, hy=None, px=None, py=None, intensity=None, waveNum=None, mode=0, surf=-1, want_opd=0, timeout=5000): """Trace large number of rays defined by their normalized field and pupil @@ -982,8 +1024,92 @@ def _test_arraytrace_module_basic(): for i in range(1, 11): print(rd[k].intensity) print("Success!") + +def _test_arrayGetTrace(): + """very basic test for the arrayGetTrace function + """ + # Basic test of the module functions + print("Basic test of arrayGetTrace module:") + x = _np.linspace(-1,1,10) + px= _np.linspace(-1,1,3) + grid = _np.meshgrid(x,x,px,px); + field= _np.transpose(grid[0:2]).reshape(-1,2); + pupil= _np.transpose(grid[2:4]).reshape(-1,2); + + (error,vigcode,pos,dir,normal,opd,intensity) = \ + zGetTraceArray_new(field,pupil,mode=0); + + print(" number of rays: %d" % len(pos)); + if len(pos)<1e5: + import matplotlib.pylab as plt + from mpl_toolkits.mplot3d import Axes3D + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + ax.scatter(*pos.T,c=opd);#_np.linalg.norm(pupil,axis=1)); + + print("Success!") + +def _test_arraytrace_vs_arrayGetTrace(): + """compare the two implementations against each other + """ + # Basic test of the module functions + print("Comparison of arraytrace and arrayGetTrace:") + nr = 441 + rd = getRayDataArray(nr) + # Fill the rest of the ray data array + pupil = 2*_np.random.rand(nr,2)-1; + field = 2*_np.random.rand(nr,2)-1; + for k in xrange(nr): + rd[k+1].x = field[k,0]; + rd[k+1].y = field[k,1]; + rd[k+1].z = pupil[k,0]; + rd[k+1].l = pupil[k,1]; + rd[k+1].intensity = 1.0; + rd[k+1].wave = 1; + rd[k+1].want_opd = 0 + # results of zArrayTrace + assert(zArrayTrace(rd)==0); + results = _np.asarray( [[r.error,r.vigcode,r.x,r.y,r.z,r.l,r.m,r.n,\ + r.Exr,r.Eyr,r.Ezr,r.opd,r.intensity] for r in rd[1:]] ); + # results of GetTraceArray + (error,vigcode,pos,dir,normal,opd,intensity) = \ + zGetTraceArray_new(field,pupil,mode=0); + # compare + def check(A,B): + isequal = _np.array_equal(A,B); + return "ok" if isequal else "failed" + print(" error : %s " % check(error,results[:,0])); + print(" vigcode : %s " % check(vigcode,results[:,1])); + print(" x,y,z : %s " % check(pos,results[:,2:5])); + print(" l,m,n : %s " % check(dir,results[:,5:8])); + print(" l2,m2,n2 : %s " % check(normal,results[:,8:11])); + print(" opd : %s " % check(opd,results[:,11])); + print(" intensity: %s " % check(intensity,results[:,12])); + + if __name__ == '__main__': - # run the test functions - _test_getRayDataArray() - _test_arraytrace_module_basic() \ No newline at end of file + # load example zemax file + import pyzdde.zdde as pyz + import os + link = pyz.createLink(); + if link is None: raise RuntimeError("Zemax DDE link could not be established."); + try: + zmxfile = os.path.join(link.zGetPath()[1], 'Sequential', 'Objectives', 'Cooke 40 degree field.zmx') + ret = link.zLoadFile(zmxfile); + if ret<>0: + raise IOError("Could not load Zemax file '%s'. Error code %d" % (zmxfile,ret)); + print("Successfully loaded zemax file: %s"%link.zGetFile()) + link.zGetUpdate() + if not link.zPushLensPermission(): + raise RuntimeError("Extensions not allowed to push lenses. Please enable in Zemax.") + link.zPushLens(1) + + # run the test functions + #_test_getRayDataArray() + #_test_arraytrace_module_basic() + _test_arrayGetTrace() + #_test_arraytrace_vs_arrayGetTrace() + finally: + link.close(); + \ No newline at end of file diff --git a/pyzdde/arraytrace/arrayTraceClient.c b/pyzdde/arraytrace/arrayTraceClient.c index b7a70df..fb9bc02 100644 --- a/pyzdde/arraytrace/arrayTraceClient.c +++ b/pyzdde/arraytrace/arrayTraceClient.c @@ -1,4 +1,4 @@ -// The code here has been adapted from the C programs zclient.c and ArrayDemo.c, +// The code here has been adapted from the C programs zclient.c and ArrayDemo.c, // which were originally written by Kenneth Moore, and they are shipped with Zemax. // zclient.c // Originally written by Kenneth Moore June 1997 @@ -20,8 +20,8 @@ DDERAYDATA *rdpGRD = NULL; DDERAYDATA *gPtr2RD = NULL; /* used for passing the ray data array to the user function */ unsigned int DdeTimeout; int RETVAL = 0; /* Return value to Python indicating general error conditions*/ - /* 0 = SUCCESS, -1 = Couldn't retrieve data in PostArrayTraceMessage, - -999 = Couldn't communicate with Zemax, -998 = timeout reached, etc*/ + /* 0 = SUCCESS, -1 = Couldn't retrieve data in PostArrayTraceMessage, */ + /* -999 = Couldn't communicate with Zemax, -998 = timeout reached, etc*/ BOOL APIENTRY DllMain(HINSTANCE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { @@ -76,7 +76,7 @@ int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) DdeTimeout = timeout; else DdeTimeout = DDE_TIMEOUT; - + hwnd = CreateWindow(szAppName, "ZEMAX Client", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, globalhInstance, NULL); UpdateWindow(hwnd); @@ -90,6 +90,62 @@ int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) return RETVAL; } +int __stdcall arrayGetTrace(int nrays, double field[][2], double pupil[][2], double intensity[], int wave_num[], int mode, int surf, int want_opd, + int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], double opd[], unsigned int timeout) +{ + int i; + // how to call this function ? + // http://scipy.github.io/old-wiki/pages/Cookbook/Ctypes#NumPy.27s_ndpointer_with_ctypes_argtypes + + // allocate memory for list of structures expected by ZEMAX + DDERAYDATA* RD = malloc((nrays+1) * sizeof(*RD)); + + // set parameters for raytrace (in 0'th element) + // see Zemax manual for meaning of the fields (do not infer from field names!) + RD[0].opd=0; // set type 0 (GetTrace) + RD[0].wave=mode; // 0 for real rays, 1 for paraxial rays + RD[0].error=nrays; + RD[0].vigcode=0; + RD[0].want_opd=surf; // surface to trace to, -1 for image, or any valid surface number + + // initialize ray-structure with initial sampling + for (i=0; i Date: Mon, 23 May 2016 17:09:19 +0200 Subject: [PATCH 02/23] increase sleep time to 100ms when waiting for Zemax results --- pyzdde/arraytrace/arrayTraceClient.c | 6 +++--- pyzdde/arraytrace/makefile.win | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pyzdde/arraytrace/arrayTraceClient.c b/pyzdde/arraytrace/arrayTraceClient.c index fb9bc02..501b19c 100644 --- a/pyzdde/arraytrace/arrayTraceClient.c +++ b/pyzdde/arraytrace/arrayTraceClient.c @@ -340,10 +340,10 @@ void WaitForData(HWND hwnd) { DispatchMessage(&msg); } - /* Give the server a chance to respond */ - Sleep(0); + /* Give the server a chance to respond: wait for 100ms */ + Sleep(100); sleep_count++; - if (sleep_count > 10000) + if (sleep_count > 10) /* check every second if timeout is reached */ { if (GetCurrentTime() - dwTime > DdeTimeout) { diff --git a/pyzdde/arraytrace/makefile.win b/pyzdde/arraytrace/makefile.win index 2f70031..73eebf8 100644 --- a/pyzdde/arraytrace/makefile.win +++ b/pyzdde/arraytrace/makefile.win @@ -67,6 +67,7 @@ help: @echo " debug32 - Creates $(PROJ).DLL for win32 architecture." @echo " debug64 - Creates $(PROJ).DLL for x64 architecture." @echo " clean - clean directory + @echo " clean-all - clean all dll's @echo. @echo "Examples: NMAKE /c /f Makefile.win" @echo " NMAKE /c /f Makefile.win release64" @@ -86,6 +87,12 @@ debug32: debug64: @$(NMAKE) ARCH="x64 Debug" release +clean-all: + @$(NMAKE) ARCH="Win32 Release" clean + @$(NMAKE) ARCH="x64 Release" clean + @$(NMAKE) ARCH="Win32 Debug" clean + @$(NMAKE) ARCH="x64 Debug" clean + clean: @-erase *.obj *.pdb *.ilk *.pch $(RELEASEDIR)\*.dll $(RELEASEDIR)\*.lib $(RELEASEDIR)\*.exp From 4ea2e0bb69526dab22967cde9b5b2bb1654f26f5 Mon Sep 17 00:00:00 2001 From: rhambach Date: Mon, 23 May 2016 20:29:24 +0200 Subject: [PATCH 03/23] rename array-trace function and add unit-tests for arraytrace.py --- pyzdde/arraytrace.py | 55 +++------ pyzdde/arraytrace/arrayTraceClient.c | 2 +- pyzdde/arraytrace/arrayTraceClient.h | 2 +- test/__init__.py | 0 test/arrayTraceTest.py | 171 +++++++++++++++++++++++++++ 5 files changed, 191 insertions(+), 39 deletions(-) create mode 100644 test/__init__.py create mode 100644 test/arrayTraceTest.py diff --git a/pyzdde/arraytrace.py b/pyzdde/arraytrace.py index 364c0c4..600b91e 100644 --- a/pyzdde/arraytrace.py +++ b/pyzdde/arraytrace.py @@ -29,7 +29,6 @@ import ctypes as _ct import collections as _co import numpy as _np -from numpy.ctypeslib import ndpointer as NPTR #import gc as _gc if _sys.version_info[0] > 2: @@ -162,7 +161,7 @@ def getRayDataArray(numRays, tType=0, mode=0, startSurf=None, endSurf=-1, return rd -def zGetTraceArray_new(field, pupil, waveNum=None, intensity=None, +def zGetTraceNumpy(field, pupil, waveNum=None, intensity=None, mode=0, surf=-1, want_opd=0, timeout=5000): # handle input arguments @@ -182,15 +181,15 @@ def zGetTraceArray_new(field, pupil, waveNum=None, intensity=None, normal=_np.zeros((nRays,3)); opd=_np.zeros(nRays); - # arrayGetTrace(int nrays, double field[][2], double pupil[][2], + # numpyGetTrace(int nrays, double field[][2], double pupil[][2], # double intensity[], int wave_num[], int mode, int surf, int want_opd, # int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], # double opd[], unsigned int timeout); - _arrayGetTrace = _array_trace_lib.arrayGetTrace - _arrayGetTrace.restype = _INT - _arrayGetTrace.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT,_INT, + _numpyGetTrace = _array_trace_lib.numpyGetTrace + _numpyGetTrace.restype = _INT + _numpyGetTrace.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT,_INT, _INT1D,_INT1D,_DBL2D,_DBL2D,_DBL2D,_DBL1D,_ct.c_uint] - _arrayGetTrace(nRays,field,pupil,intensity,waveNum,mode,surf,want_opd, + _numpyGetTrace(nRays,field,pupil,intensity,waveNum,mode,surf,want_opd, error,vigcode,pos,dir,normal,opd,timeout) return (error,vigcode,pos,dir,normal,opd,intensity); @@ -1025,11 +1024,11 @@ def _test_arraytrace_module_basic(): print(rd[k].intensity) print("Success!") -def _test_arrayGetTrace(): - """very basic test for the arrayGetTrace function +def _test_zGetTraceNumpy(): + """very basic test for the zGetTraceNumpy function """ # Basic test of the module functions - print("Basic test of arrayGetTrace module:") + print("Basic test of zGetTraceNumpy module:") x = _np.linspace(-1,1,10) px= _np.linspace(-1,1,3) grid = _np.meshgrid(x,x,px,px); @@ -1037,7 +1036,7 @@ def _test_arrayGetTrace(): pupil= _np.transpose(grid[2:4]).reshape(-1,2); (error,vigcode,pos,dir,normal,opd,intensity) = \ - zGetTraceArray_new(field,pupil,mode=0); + zGetTraceNumpy(field,pupil,mode=0); print(" number of rays: %d" % len(pos)); if len(pos)<1e5: @@ -1049,11 +1048,11 @@ def _test_arrayGetTrace(): print("Success!") -def _test_arraytrace_vs_arrayGetTrace(): +def _test_zArrayTrace_vs_zGetTraceNumpy(): """compare the two implementations against each other """ # Basic test of the module functions - print("Comparison of arraytrace and arrayGetTrace:") + print("Comparison of zArrayTrace and zGetTraceNumpy:") nr = 441 rd = getRayDataArray(nr) # Fill the rest of the ray data array @@ -1073,7 +1072,7 @@ def _test_arraytrace_vs_arrayGetTrace(): r.Exr,r.Eyr,r.Ezr,r.opd,r.intensity] for r in rd[1:]] ); # results of GetTraceArray (error,vigcode,pos,dir,normal,opd,intensity) = \ - zGetTraceArray_new(field,pupil,mode=0); + zGetTraceNumpy(field,pupil,mode=0); # compare def check(A,B): isequal = _np.array_equal(A,B); @@ -1089,27 +1088,9 @@ def check(A,B): if __name__ == '__main__': - # load example zemax file - import pyzdde.zdde as pyz - import os - link = pyz.createLink(); - if link is None: raise RuntimeError("Zemax DDE link could not be established."); - try: - zmxfile = os.path.join(link.zGetPath()[1], 'Sequential', 'Objectives', 'Cooke 40 degree field.zmx') - ret = link.zLoadFile(zmxfile); - if ret<>0: - raise IOError("Could not load Zemax file '%s'. Error code %d" % (zmxfile,ret)); - print("Successfully loaded zemax file: %s"%link.zGetFile()) - link.zGetUpdate() - if not link.zPushLensPermission(): - raise RuntimeError("Extensions not allowed to push lenses. Please enable in Zemax.") - link.zPushLens(1) - - # run the test functions - #_test_getRayDataArray() - #_test_arraytrace_module_basic() - _test_arrayGetTrace() - #_test_arraytrace_vs_arrayGetTrace() - finally: - link.close(); + # run the test functions + _test_getRayDataArray() + _test_arraytrace_module_basic() + _test_zGetTraceNumpy() + _test_zArrayTrace_vs_zGetTraceNumpy() \ No newline at end of file diff --git a/pyzdde/arraytrace/arrayTraceClient.c b/pyzdde/arraytrace/arrayTraceClient.c index 501b19c..fb474fd 100644 --- a/pyzdde/arraytrace/arrayTraceClient.c +++ b/pyzdde/arraytrace/arrayTraceClient.c @@ -90,7 +90,7 @@ int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) return RETVAL; } -int __stdcall arrayGetTrace(int nrays, double field[][2], double pupil[][2], double intensity[], int wave_num[], int mode, int surf, int want_opd, +int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], double intensity[], int wave_num[], int mode, int surf, int want_opd, int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], double opd[], unsigned int timeout) { int i; diff --git a/pyzdde/arraytrace/arrayTraceClient.h b/pyzdde/arraytrace/arrayTraceClient.h index b38b4cf..b6050d5 100644 --- a/pyzdde/arraytrace/arrayTraceClient.h +++ b/pyzdde/arraytrace/arrayTraceClient.h @@ -30,7 +30,7 @@ char *GetString(char *szBuffer, int n, char *szSubString); int PostArrayTraceMessage(char *szBuffer, DDERAYDATA *RD); void rayTraceFunction(); DLL_EXPORT int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout); -DLL_EXPORT int __stdcall arrayGetTrace(int nrays, double field[][2], double pupil[][2], +DLL_EXPORT int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], double intensity[], int wave_num[], int mode, int surf, int want_opd, int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], double opd[], unsigned int timeout); diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/arrayTraceTest.py b/test/arrayTraceTest.py new file mode 100644 index 0000000..72077a9 --- /dev/null +++ b/test/arrayTraceTest.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Name: ArrayTraceUnittest.py +# Purpose: unit tests for ArrayTrace +# +# Licence: MIT License +# This file is subject to the terms and conditions of the MIT License. +# For further details, please refer to LICENSE.txt +#------------------------------------------------------------------------------- +from __future__ import division +from __future__ import print_function +import os +import sys +import unittest +import numpy as np + +import pyzdde.zdde as pyzdde +import pyzdde.arraytrace as at +from test.pyZDDEunittest import get_test_file + +class TestArrayTrace(unittest.TestCase): + + @classmethod + def setUpClass(self): + print('RUNNING TESTS FOR MODULE \'%s\'.'% at.__file__) + self.ln = pyzdde.createLink(); + self.ln.zGetUpdate(); + + @classmethod + def tearDownClass(self): + self.ln.close(); + + + def test_getRayDataArray(self): + print("\nTEST: arraytrace.getRayDataArray()") + + # create RayData without any kwargs + rd = at.getRayDataArray(numRays=5) + self.assertEqual(len(rd), 6) + self.assertEqual(rd[0].error, 5) # number of rays + self.assertEqual(rd[0].opd, 0) # GetTrace ray tracing type + self.assertEqual(rd[0].wave, 0) # real ray tracing + self.assertEqual(rd[0].want_opd,-1) # last surface + + # create RayData with some more arguments + rd = at.getRayDataArray(numRays=5, tType=3, mode=1, startSurf=2) + self.assertEqual(rd[0].opd, 3) # mode 3 + self.assertEqual(rd[0].wave,1) # real ray tracing + self.assertEqual(rd[0].vigcode,2) # first surface + + # create RayData with kwargs + rd = at.getRayDataArray(numRays=5, tType=2, x=1.0, y=1.0) + self.assertEqual(rd[0].x,1.0) + self.assertEqual(rd[0].y,1.0) + self.assertEqual(rd[0].z,0.0) + + # create RayData with kwargs overriding some regular parameters + rd = at.getRayDataArray(numRays=5, tType=2, x=1.0, y=1.0, error=1) + self.assertEqual(len(rd),6) + self.assertEqual(rd[0].x,1.0) + self.assertEqual(rd[0].y,1.0) + self.assertEqual(rd[0].z,0.0) + self.assertEqual(rd[0].error,1) + + def test_zArrayTrace(self): + print("\nTEST: arraytrace.zArrayTrace()") + # Load a lens file into the DDE server + filename = get_test_file() + self.ln.zLoadFile(filename) + self.ln.zGetUpdate(); + + nr = 9 + rd = at.getRayDataArray(nr) + # Fill the rest of the ray data array + k = 0 + for i in xrange(-10, 11, 10): + for j in xrange(-10, 11, 10): + k += 1 + rd[k].z = i/20.0 # px + rd[k].l = j/20.0 # py + rd[k].intensity = 1.0 + rd[k].wave = 1 + rd[k].want_opd = 0 + # run array trace (C-extension) + ret = at.zArrayTrace(rd) + self.assertEqual(ret,0); + #r = rd[1]; # select first ray + #for key in ('error','vigcode','x','y','z','l','m','n','Exr','Eyr','Ezr','opd','intensity'): + # print('self.assertAlmostEqual(rd[1].%s,%.8g,msg=\'%s differs\');'%(key,getattr(rd[1],key),key)); + self.assertAlmostEqual(rd[1].error,0,msg='error differs'); + self.assertAlmostEqual(rd[1].vigcode,0,msg='vigcode differs'); + self.assertAlmostEqual(rd[1].x,-0.0029856861,msg='x differs'); + self.assertAlmostEqual(rd[1].y,-0.0029856861,msg='y differs'); + self.assertAlmostEqual(rd[1].z,0,msg='z differs'); + self.assertAlmostEqual(rd[1].l,0.050136296,msg='l differs'); + self.assertAlmostEqual(rd[1].m,0.050136296,msg='m differs'); + self.assertAlmostEqual(rd[1].n,0.99748318,msg='n differs'); + self.assertAlmostEqual(rd[1].Exr,0,msg='Exr differs'); + self.assertAlmostEqual(rd[1].Eyr,0,msg='Eyr differs'); + self.assertAlmostEqual(rd[1].Ezr,-1,msg='Ezr differs'); + self.assertAlmostEqual(rd[1].opd,64.711234,msg='opd differs',places=5); + self.assertAlmostEqual(rd[1].intensity,1,msg='intensity differs'); + + + + def test_zGetTraceNumpy(self): + print("\nTEST: arraytrace.zGetTraceNumpy()") + # Load a lens file into the DDE server + filename = get_test_file() + self.ln.zLoadFile(filename) + self.ln.zGetUpdate(); + # set-up field and pupil sampling + x = np.linspace(-1,1,3) + px= np.linspace(-1,1,3) + grid = np.meshgrid(x,x,px,px); + field= np.transpose(grid[0:2]).reshape(-1,2); + pupil= np.transpose(grid[2:4]).reshape(-1,2); + # array trace (C-extension) + ret = at.zGetTraceNumpy(field,pupil,mode=0); + self.assertEqual(len(field),3**4); + + #for i in xrange(len(ret)): + # name = ['error','vigcode','pos','dir','normal','opd','intensity'] + # print('self.assertAlmostEqual(ret[%d][1],%s,msg=\'%s differs\');'%(i,str(ret[i][1]),name[i])); + self.assertEqual(ret[0][1],0,msg='error differs'); + self.assertEqual(ret[1][1],3,msg='vigcode differs'); + self.assertTrue(np.allclose(ret[2][1],[-18.24210131, -0.0671553, 0.]),msg='pos differs'); + self.assertTrue(np.allclose(ret[3][1],[-0.24287826, 0.09285061, 0.96560288]),msg='dir differs'); + self.assertTrue(np.allclose(ret[4][1],[ 0, 0, -1]),msg='normal differs'); + self.assertAlmostEqual(ret[5][1],66.8437599679,msg='opd differs'); + self.assertAlmostEqual(ret[6][1],1.0,msg='intensity differs'); + + + def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): + print("\nTEST: comparison of zArrayTrace and zGetTraceNumpy") + nr = 22; + rd = at.getRayDataArray(nr) + # Fill the rest of the ray data array + pupil = 2*np.random.rand(nr,2)-1; + field = 2*np.random.rand(nr,2)-1; + for k in xrange(nr): + rd[k+1].x = field[k,0]; + rd[k+1].y = field[k,1]; + rd[k+1].z = pupil[k,0]; + rd[k+1].l = pupil[k,1]; + rd[k+1].intensity = 1.0; + rd[k+1].wave = 1; + rd[k+1].want_opd = 0 + # results of zArrayTrace + ret = at.zArrayTrace(rd); + self.assertEqual(ret,0); + results = np.asarray( [[r.error,r.vigcode,r.x,r.y,r.z,r.l,r.m,r.n,\ + r.Exr,r.Eyr,r.Ezr,r.opd,r.intensity] for r in rd[1:]] ); + # results of GetTraceArray + (error,vigcode,pos,dir,normal,opd,intensity) = \ + at.zGetTraceNumpy(field,pupil,mode=0); + + # compare + self.assertTrue(np.array_equal(error,results[:,0]),msg="error differs"); + self.assertTrue(np.array_equal(vigcode,results[:,1]),msg="vigcode differs"); + self.assertTrue(np.array_equal(pos,results[:,2:5]),msg="pos differs"); + self.assertTrue(np.array_equal(dir,results[:,5:8]),msg="dir differs"); + self.assertTrue(np.array_equal(normal,results[:,8:11]),msg="normal differs"); + self.assertTrue(np.array_equal(opd,results[:,11]),msg="opd differs"); + self.assertTrue(np.array_equal(intensity,results[:,12]),msg="intensity differs"); + + + + +if __name__ == '__main__': + unittest.main() From 07b1be1141dba6658a5f48dc6c4673893a687b17 Mon Sep 17 00:00:00 2001 From: rhambach Date: Mon, 23 May 2016 21:35:14 +0200 Subject: [PATCH 04/23] add documentation for zGetTraceNumpy and allow Retval to indicate timeout --- pyzdde/arraytrace.py | 149 ++++++++++++++++++++------- pyzdde/arraytrace/arrayTraceClient.c | 8 +- pyzdde/arraytrace/arrayTraceClient.h | 2 +- test/arrayTraceTest.py | 17 +-- 4 files changed, 128 insertions(+), 48 deletions(-) diff --git a/pyzdde/arraytrace.py b/pyzdde/arraytrace.py index 600b91e..fea1cba 100644 --- a/pyzdde/arraytrace.py +++ b/pyzdde/arraytrace.py @@ -78,14 +78,13 @@ def zArrayTrace(rd, timeout=5000): ray tracing. Use the helper function getRayDataArray() to generate ``rd`` timeout : integer - time in milliseconds (Default = 5000) + time in milliseconds (Default = 5s), at least 1s Returns ------- ret : integer - Error codes meaning 0 = SUCCESS, -1 = Couldn't retrieve data in - PostArrayTraceMessage, -999 = Couldn't communicate with Zemax, - -998 = timeout reached + Error codes meaning 0 = SUCCESS, + -999 = Couldn't communicate with Zemax, -998 = timeout reached """ return _arrayTrace(rd, int(timeout)) @@ -161,38 +160,118 @@ def getRayDataArray(numRays, tType=0, mode=0, startSurf=None, endSurf=-1, return rd -def zGetTraceNumpy(field, pupil, waveNum=None, intensity=None, - mode=0, surf=-1, want_opd=0, timeout=5000): - - # handle input arguments - assert 2 == field.ndim == pupil.ndim, 'field and pupil should be 2d arrays' - assert field.shape == pupil.shape, 'we expect field and pupil points for each ray' - nRays = field.shape[0]; - if waveNum is None: waveNum=1; - if _np.isscalar(waveNum): waveNum=_np.zeros(nRays,dtype=_np.int)+waveNum; - if intensity is None: intensity=1; - if _np.isscalar(intensity): intensity=_np.zeros(nRays)+intensity; +def zGetTraceNumpy(field, pupil, intensity=None, waveNum=None, + mode=0, surf=-1, want_opd=0, timeout=60000): + """Trace large number of rays defined by their normalized field and pupil + coordinates on lens file in the LDE of main Zemax application (not in the DDE server) + + Parameters + ---------- + field : ndarray of shape (``numRays``,2) + list of normalized field heights along x and y axis + px : ndarray of shape (``numRays``,2) + list of normalized heights in pupil coordinates, along x and y axis + intensity : float or vector of length ``numRays``, optional + initial intensities. If a vector of length ``numRays`` is given it is + used. If a single float value is passed, all rays use the same value for + their initial intensities. Default: all intensities are set to ``1.0``. + waveNum : integer or vector of length ``numRays``, optional + wavelength number. If a vector of integers of length ``numRays`` is given + it is used. If a single integer value is passed, all rays use the same + value for wavelength number. Default: wavelength number equal to 1. + mode : integer, optional + 0 = real (Default), 1 = paraxial + surf : integer, optional + surface to trace the ray to. Usually, the ray data is only needed at + the image surface (``surf = -1``, default) + want_opd : integer, optional + 0 if OPD data is not needed (Default), 1 if it is. See Zemax manual + for details. + timeout : integer, optional + command timeout specified in milli-seconds (default: 1min), at least 1s + + Returns + ------- + error : list of integers + * ``0`` = ray traced successfully; + * ``+ve`` number = the ray missed the surface; + * ``-ve`` number = the ray total internal reflected (TIR) at surface \ + given by the absolute value of the ``error`` + vigcode : list of integers + the first surface where the ray was vignetted. Unless an error occurs + at that surface or subsequent to that surface, the ray will continue + to trace to the requested surface. + pos : ndarray of shape (``numRays``,3) + local coordinates ``(x,y,z)`` of each ray on the requested surface + dir : ndarray of shape (``numRays``,3) + local direction cosines ``(l,m,n)`` after refraction into the media + following the requested surface. + normal : ndarray of shape (``numRays``,3) + local direction cosines ``(l2,m2,n2)`` of the surface normals at the + intersection point of the ray with the requested surface + opd : list of reals + computed optical path difference if ``want_opd <> 0`` + intensity : list of reals + the relative transmitted intensity of the ray, including any pupil + or surface apodization defined. + + If ray tracing fails, an RuntimeError is raised. + + Examples + -------- + >>> import numpy as np + >>> import matplotlib.pylab as plt + >>> # cartesian sampling in field an pupil + >>> x = np.linspace(-1,1,10) + >>> px= np.linspace(-1,1,3) + >>> grid = np.meshgrid(x,x,px,px); + >>> field= np.transpose(grid[0:2]).reshape(-1,2); + >>> pupil= np.transpose(grid[2:4]).reshape(-1,2); + >>> # run array-trace + >>> (error,vigcode,pos,dir,normal,opd,intensity) = \\ + >>> zGetTraceNumpy(field,pupil,mode=0); + >>> # plot results + >>> plt.scatter(pos[:,0],pos[:,1]) + + Notes + ----- + The opd can only be computed if the last surface is the image surface, + otherwise, the opd value will be zero. + """ + # handle input arguments + assert 2 == field.ndim == pupil.ndim, 'field and pupil should be 2d arrays' + assert field.shape == pupil.shape, 'we expect field and pupil points for each ray' + nRays = field.shape[0]; + if intensity is None: intensity=1; + if _np.isscalar(intensity): intensity=_np.zeros(nRays)+intensity; + if waveNum is None: waveNum=1; + if _np.isscalar(waveNum): waveNum=_np.zeros(nRays,dtype=_np.int)+waveNum; + + + # set up output arguments + error=_np.zeros(nRays,dtype=_np.int); + vigcode=_np.zeros(nRays,dtype=_np.int); + pos=_np.zeros((nRays,3)); + dir=_np.zeros((nRays,3)); + normal=_np.zeros((nRays,3)); + opd=_np.zeros(nRays); - # set up output arguments - error=_np.zeros(nRays,dtype=_np.int); - vigcode=_np.zeros(nRays,dtype=_np.int); - pos=_np.zeros((nRays,3)); - dir=_np.zeros((nRays,3)); - normal=_np.zeros((nRays,3)); - opd=_np.zeros(nRays); - - # numpyGetTrace(int nrays, double field[][2], double pupil[][2], - # double intensity[], int wave_num[], int mode, int surf, int want_opd, - # int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], - # double opd[], unsigned int timeout); - _numpyGetTrace = _array_trace_lib.numpyGetTrace - _numpyGetTrace.restype = _INT - _numpyGetTrace.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT,_INT, - _INT1D,_INT1D,_DBL2D,_DBL2D,_DBL2D,_DBL1D,_ct.c_uint] - _numpyGetTrace(nRays,field,pupil,intensity,waveNum,mode,surf,want_opd, - error,vigcode,pos,dir,normal,opd,timeout) - - return (error,vigcode,pos,dir,normal,opd,intensity); + # numpyGetTrace(int nrays, double field[][2], double pupil[][2], + # double intensity[], int wave_num[], int mode, int surf, int want_opd, + # int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], + # double opd[], unsigned int timeout); + _numpyGetTrace = _array_trace_lib.numpyGetTrace + _numpyGetTrace.restype = _INT + _numpyGetTrace.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT,_INT, + _INT1D,_INT1D,_DBL2D,_DBL2D,_DBL2D,_DBL1D,_ct.c_uint] + ret = _numpyGetTrace(nRays,field,pupil,intensity,waveNum,mode,surf,want_opd, + error,vigcode,pos,dir,normal,opd,timeout) + # analyse error - flag + if ret==-1: raise RuntimeError("Couldn't retrieve data in PostArrayTraceMessage.") + if ret==-999: raise RuntimeError("Couldn't communicate with Zemax."); + if ret==-998: raise RuntimeError("Timeout reached after %dms"%timeout); + + return (error,vigcode,pos,dir,normal,opd,intensity); def zGetTraceArray(numRays, hx=None, hy=None, px=None, py=None, intensity=None, waveNum=None, mode=0, surf=-1, want_opd=0, timeout=5000): diff --git a/pyzdde/arraytrace/arrayTraceClient.c b/pyzdde/arraytrace/arrayTraceClient.c index fb474fd..e8c6a48 100644 --- a/pyzdde/arraytrace/arrayTraceClient.c +++ b/pyzdde/arraytrace/arrayTraceClient.c @@ -20,8 +20,7 @@ DDERAYDATA *rdpGRD = NULL; DDERAYDATA *gPtr2RD = NULL; /* used for passing the ray data array to the user function */ unsigned int DdeTimeout; int RETVAL = 0; /* Return value to Python indicating general error conditions*/ - /* 0 = SUCCESS, -1 = Couldn't retrieve data in PostArrayTraceMessage, */ - /* -999 = Couldn't communicate with Zemax, -998 = timeout reached, etc*/ + /* 0 = SUCCESS, -999 = Couldn't communicate with Zemax, -998 = timeout reached, etc*/ BOOL APIENTRY DllMain(HINSTANCE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { @@ -44,7 +43,6 @@ void rayTraceFunction(void) static char szBuffer[5000]; int ret = 0; ret = PostArrayTraceMessage(szBuffer, gPtr2RD); - RETVAL = ret; /* ret = -1, if couldn't get data*/ /* clear the pointer */ gPtr2RD = NULL; } @@ -330,7 +328,7 @@ void WaitForData(HWND hwnd) int sleep_count; MSG msg; DWORD dwTime; - dwTime = GetCurrentTime(); + dwTime = GetTickCount(); sleep_count = 0; @@ -345,7 +343,7 @@ void WaitForData(HWND hwnd) sleep_count++; if (sleep_count > 10) /* check every second if timeout is reached */ { - if (GetCurrentTime() - dwTime > DdeTimeout) + if (GetTickCount() - dwTime > DdeTimeout) { printf("\nTimeout reached!\n"); /* will be visible in stdout*/ RETVAL = -998; /* indicate to python calling function */ diff --git a/pyzdde/arraytrace/arrayTraceClient.h b/pyzdde/arraytrace/arrayTraceClient.h index b6050d5..bd05c90 100644 --- a/pyzdde/arraytrace/arrayTraceClient.h +++ b/pyzdde/arraytrace/arrayTraceClient.h @@ -8,7 +8,7 @@ #define DLL_EXPORT __declspec(dllexport) #define WM_USER_INITIATE (WM_USER + 1) -#define DDE_TIMEOUT 50000 +#define DDE_TIMEOUT 1000 #pragma warning ( disable : 4996 ) // functions like strcpy are now deprecated for security reasons; this disables the warning #pragma comment(lib, "User32.lib") #pragma comment(lib, "gdi32.lib") diff --git a/test/arrayTraceTest.py b/test/arrayTraceTest.py index 72077a9..c457de3 100644 --- a/test/arrayTraceTest.py +++ b/test/arrayTraceTest.py @@ -64,14 +64,13 @@ def test_getRayDataArray(self): def test_zArrayTrace(self): print("\nTEST: arraytrace.zArrayTrace()") - # Load a lens file into the DDE server + # Load a lens file into the Lde filename = get_test_file() self.ln.zLoadFile(filename) - self.ln.zGetUpdate(); - + self.ln.zPushLens(1); + # set up field and pupil sampling nr = 9 rd = at.getRayDataArray(nr) - # Fill the rest of the ray data array k = 0 for i in xrange(-10, 11, 10): for j in xrange(-10, 11, 10): @@ -105,10 +104,10 @@ def test_zArrayTrace(self): def test_zGetTraceNumpy(self): print("\nTEST: arraytrace.zGetTraceNumpy()") - # Load a lens file into the DDE server + # Load a lens file into the LDE filename = get_test_file() self.ln.zLoadFile(filename) - self.ln.zGetUpdate(); + self.ln.zPushLens(1); # set-up field and pupil sampling x = np.linspace(-1,1,3) px= np.linspace(-1,1,3) @@ -133,9 +132,13 @@ def test_zGetTraceNumpy(self): def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): print("\nTEST: comparison of zArrayTrace and zGetTraceNumpy") + # Load a lens file into the LDE + filename = get_test_file() + self.ln.zLoadFile(filename) + self.ln.zPushLens(1); + # set-up field and pupil sampling nr = 22; rd = at.getRayDataArray(nr) - # Fill the rest of the ray data array pupil = 2*np.random.rand(nr,2)-1; field = 2*np.random.rand(nr,2)-1; for k in xrange(nr): From de1e519c48a323ec600477f5ee82158acbacaae5 Mon Sep 17 00:00:00 2001 From: rhambach Date: Tue, 24 May 2016 18:25:13 +0200 Subject: [PATCH 05/23] restructure code, remove dll's from git-repo for now - split arraytrace.py into functions using the DDERAYDATA structure as arguments and functions using numpy arrays as arguments - organize everything into a submodule called arraytrace - backward compatibility is preserved by import in arraytrace/__init__.py - adapt unittests --- pyzdde/arraytrace/Release/ArrayTrace.dll | Bin 78336 -> 0 bytes pyzdde/arraytrace/__init__.py | 3 + pyzdde/arraytrace/numpy_interface.py | 196 +++++++++++++++++ .../raystruct_interface.py} | 198 +----------------- pyzdde/arraytrace/x64/Release/ArrayTrace.dll | Bin 90112 -> 0 bytes test/arrayTraceTest.py | 6 +- 6 files changed, 210 insertions(+), 193 deletions(-) delete mode 100644 pyzdde/arraytrace/Release/ArrayTrace.dll create mode 100644 pyzdde/arraytrace/__init__.py create mode 100644 pyzdde/arraytrace/numpy_interface.py rename pyzdde/{arraytrace.py => arraytrace/raystruct_interface.py} (83%) delete mode 100644 pyzdde/arraytrace/x64/Release/ArrayTrace.dll diff --git a/pyzdde/arraytrace/Release/ArrayTrace.dll b/pyzdde/arraytrace/Release/ArrayTrace.dll deleted file mode 100644 index 1ec5b375e3224a69bc2facb85d55fa2f2589c838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78336 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 2: + xrange = range + +def _is64bit(): + """return True if Python version is 64 bit + """ + return _sys.maxsize > 2**31 - 1 + +# load the arrayTrace library +_dllDir = "x64\\Release\\" if _is64bit() else "win32\\Release\\" +_dllName = "ArrayTrace.dll" +_dllpath = _os.path.join(_os.path.dirname(_os.path.realpath(__file__)), _dllDir) +_array_trace_lib = _ct.WinDLL(_dllpath + _dllName) + +# shorthands for CTypes +_INT = _ct.c_int; +_INT1D = _np.ctypeslib.ndpointer(ndim=1,dtype=_np.int,flags=["C_CONTIGUOUS","ALIGNED"]) +_INT2D = _np.ctypeslib.ndpointer(ndim=2,dtype=_np.int,flags=["C_CONTIGUOUS","ALIGNED"]) +_DBL1D = _np.ctypeslib.ndpointer(ndim=1,dtype=_np.double,flags=["C_CONTIGUOUS","ALIGNED"]) +_DBL2D = _np.ctypeslib.ndpointer(ndim=2,dtype=_np.double,flags=["C_CONTIGUOUS","ALIGNED"]) + +def zGetTraceArray(field, pupil, intensity=None, waveNum=None, + mode=0, surf=-1, want_opd=0, timeout=60000): + """Trace large number of rays defined by their normalized field and pupil + coordinates on lens file in the LDE of main Zemax application (not in the DDE server) + + Parameters + ---------- + field : ndarray of shape (``numRays``,2) + list of normalized field heights along x and y axis + px : ndarray of shape (``numRays``,2) + list of normalized heights in pupil coordinates, along x and y axis + intensity : float or vector of length ``numRays``, optional + initial intensities. If a vector of length ``numRays`` is given it is + used. If a single float value is passed, all rays use the same value for + their initial intensities. Default: all intensities are set to ``1.0``. + waveNum : integer or vector of length ``numRays``, optional + wavelength number. If a vector of integers of length ``numRays`` is given + it is used. If a single integer value is passed, all rays use the same + value for wavelength number. Default: wavelength number equal to 1. + mode : integer, optional + 0 = real (Default), 1 = paraxial + surf : integer, optional + surface to trace the ray to. Usually, the ray data is only needed at + the image surface (``surf = -1``, default) + want_opd : integer, optional + 0 if OPD data is not needed (Default), 1 if it is. See Zemax manual + for details. + timeout : integer, optional + command timeout specified in milli-seconds (default: 1min), at least 1s + + Returns + ------- + error : list of integers + * ``0`` = ray traced successfully; + * ``+ve`` number = the ray missed the surface; + * ``-ve`` number = the ray total internal reflected (TIR) at surface \ + given by the absolute value of the ``error`` + vigcode : list of integers + the first surface where the ray was vignetted. Unless an error occurs + at that surface or subsequent to that surface, the ray will continue + to trace to the requested surface. + pos : ndarray of shape (``numRays``,3) + local coordinates ``(x,y,z)`` of each ray on the requested surface + dir : ndarray of shape (``numRays``,3) + local direction cosines ``(l,m,n)`` after refraction into the media + following the requested surface. + normal : ndarray of shape (``numRays``,3) + local direction cosines ``(l2,m2,n2)`` of the surface normals at the + intersection point of the ray with the requested surface + opd : list of reals + computed optical path difference if ``want_opd <> 0`` + intensity : list of reals + the relative transmitted intensity of the ray, including any pupil + or surface apodization defined. + + If ray tracing fails, an RuntimeError is raised. + + Examples + -------- + >>> import numpy as np + >>> import matplotlib.pylab as plt + >>> # cartesian sampling in field an pupil + >>> x = np.linspace(-1,1,10) + >>> px= np.linspace(-1,1,3) + >>> grid = np.meshgrid(x,x,px,px); + >>> field= np.transpose(grid[0:2]).reshape(-1,2); + >>> pupil= np.transpose(grid[2:4]).reshape(-1,2); + >>> # run array-trace + >>> (error,vigcode,pos,dir,normal,opd,intensity) = \\ + >>> zGetTraceNumpy(field,pupil,mode=0); + >>> # plot results + >>> plt.scatter(pos[:,0],pos[:,1]) + + Notes + ----- + The opd can only be computed if the last surface is the image surface, + otherwise, the opd value will be zero. + """ + # handle input arguments + assert 2 == field.ndim == pupil.ndim, 'field and pupil should be 2d arrays' + assert field.shape == pupil.shape, 'we expect field and pupil points for each ray' + nRays = field.shape[0]; + if intensity is None: intensity=1; + if _np.isscalar(intensity): intensity=_np.zeros(nRays)+intensity; + if waveNum is None: waveNum=1; + if _np.isscalar(waveNum): waveNum=_np.zeros(nRays,dtype=_np.int)+waveNum; + + + # set up output arguments + error=_np.zeros(nRays,dtype=_np.int); + vigcode=_np.zeros(nRays,dtype=_np.int); + pos=_np.zeros((nRays,3)); + dir=_np.zeros((nRays,3)); + normal=_np.zeros((nRays,3)); + opd=_np.zeros(nRays); + + # numpyGetTrace(int nrays, double field[][2], double pupil[][2], + # double intensity[], int wave_num[], int mode, int surf, int want_opd, + # int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], + # double opd[], unsigned int timeout); + _numpyGetTrace = _array_trace_lib.numpyGetTrace + _numpyGetTrace.restype = _INT + _numpyGetTrace.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT,_INT, + _INT1D,_INT1D,_DBL2D,_DBL2D,_DBL2D,_DBL1D,_ct.c_uint] + ret = _numpyGetTrace(nRays,field,pupil,intensity,waveNum,mode,surf,want_opd, + error,vigcode,pos,dir,normal,opd,timeout) + # analyse error - flag + if ret==-1: raise RuntimeError("Couldn't retrieve data in PostArrayTraceMessage.") + if ret==-999: raise RuntimeError("Couldn't communicate with Zemax."); + if ret==-998: raise RuntimeError("Timeout reached after %dms"%timeout); + + return (error,vigcode,pos,dir,normal,opd,intensity); + + + +# ########################################################################### +# Basic test functions +# Please note that some of the following tests require Zemax to be running +# In addition, load a lens file into Zemax +# ########################################################################### + +def _test_zGetTraceArray(): + """very basic test for the zGetTraceNumpy function + """ + # Basic test of the module functions + print("Basic test of zGetTraceNumpy module:") + x = _np.linspace(-1,1,10) + px= _np.linspace(-1,1,3) + grid = _np.meshgrid(x,x,px,px); + field= _np.transpose(grid[0:2]).reshape(-1,2); + pupil= _np.transpose(grid[2:4]).reshape(-1,2); + + (error,vigcode,pos,dir,normal,opd,intensity) = \ + zGetTraceArray(field,pupil,mode=0); + + print(" number of rays: %d" % len(pos)); + if len(pos)<1e5: + import matplotlib.pylab as plt + from mpl_toolkits.mplot3d import Axes3D + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + ax.scatter(*pos.T,c=opd);#_np.linalg.norm(pupil,axis=1)); + + print("Success!") + + + + +if __name__ == '__main__': + # run the test functions + _test_zGetTraceArray() + \ No newline at end of file diff --git a/pyzdde/arraytrace.py b/pyzdde/arraytrace/raystruct_interface.py similarity index 83% rename from pyzdde/arraytrace.py rename to pyzdde/arraytrace/raystruct_interface.py index fea1cba..3a099f8 100644 --- a/pyzdde/arraytrace.py +++ b/pyzdde/arraytrace/raystruct_interface.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- #------------------------------------------------------------------------------- -# Name: arraytrace.py -# Purpose: Module for doing array ray tracing in Zemax. +# Name: raystruct_interface.py +# Purpose: Module for doing array ray tracing in Zemax using DDERAYDATA struct +# to pass ray data to Zemax # Licence: MIT License # This file is subject to the terms and conditions of the MIT License. # For further details, please refer to LICENSE.txt @@ -14,7 +15,7 @@ 2. getRayDataArray() -- Helper function that creates the ctypes ray data structure array, fills up the first element and returns the array -In addition the following helper functions are provided that supports 5 different +In addition the following wrapper functions are provided that supports 5 different modes discussed in the Zemax manual 1. zGetTraceArray() @@ -28,7 +29,6 @@ import sys as _sys import ctypes as _ct import collections as _co -import numpy as _np #import gc as _gc if _sys.version_info[0] > 2: @@ -50,21 +50,15 @@ def _is64bit(): """ return _sys.maxsize > 2**31 - 1 -_dllDir = "arraytrace\\x64\\Release\\" if _is64bit() else "arraytrace\\win32\\Release\\" +_dllDir = "x64\\Release\\" if _is64bit() else "win32\\Release\\" _dllName = "ArrayTrace.dll" _dllpath = _os.path.join(_os.path.dirname(_os.path.realpath(__file__)), _dllDir) # load the arrayTrace library _array_trace_lib = _ct.WinDLL(_dllpath + _dllName) -# shorthands for Types -_INT = _ct.c_int; -_INT1D = _np.ctypeslib.ndpointer(ndim=1,dtype=_np.int,flags=["C_CONTIGUOUS","ALIGNED"]) -_INT2D = _np.ctypeslib.ndpointer(ndim=2,dtype=_np.int,flags=["C_CONTIGUOUS","ALIGNED"]) -_DBL1D = _np.ctypeslib.ndpointer(ndim=1,dtype=_np.double,flags=["C_CONTIGUOUS","ALIGNED"]) -_DBL2D = _np.ctypeslib.ndpointer(ndim=2,dtype=_np.double,flags=["C_CONTIGUOUS","ALIGNED"]) # int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) _arrayTrace = _array_trace_lib.arrayTrace -_arrayTrace.restype = _INT -_arrayTrace.argtypes = [_ct.POINTER(DdeArrayData), _INT] +_arrayTrace.restype = _ct.c_int +_arrayTrace.argtypes = [_ct.POINTER(DdeArrayData), _ct.c_int] def zArrayTrace(rd, timeout=5000): @@ -159,120 +153,6 @@ def getRayDataArray(numRays, tType=0, mode=0, startSurf=None, endSurf=-1, setattr(rd[0], k, kwargs[k]) return rd - -def zGetTraceNumpy(field, pupil, intensity=None, waveNum=None, - mode=0, surf=-1, want_opd=0, timeout=60000): - """Trace large number of rays defined by their normalized field and pupil - coordinates on lens file in the LDE of main Zemax application (not in the DDE server) - - Parameters - ---------- - field : ndarray of shape (``numRays``,2) - list of normalized field heights along x and y axis - px : ndarray of shape (``numRays``,2) - list of normalized heights in pupil coordinates, along x and y axis - intensity : float or vector of length ``numRays``, optional - initial intensities. If a vector of length ``numRays`` is given it is - used. If a single float value is passed, all rays use the same value for - their initial intensities. Default: all intensities are set to ``1.0``. - waveNum : integer or vector of length ``numRays``, optional - wavelength number. If a vector of integers of length ``numRays`` is given - it is used. If a single integer value is passed, all rays use the same - value for wavelength number. Default: wavelength number equal to 1. - mode : integer, optional - 0 = real (Default), 1 = paraxial - surf : integer, optional - surface to trace the ray to. Usually, the ray data is only needed at - the image surface (``surf = -1``, default) - want_opd : integer, optional - 0 if OPD data is not needed (Default), 1 if it is. See Zemax manual - for details. - timeout : integer, optional - command timeout specified in milli-seconds (default: 1min), at least 1s - - Returns - ------- - error : list of integers - * ``0`` = ray traced successfully; - * ``+ve`` number = the ray missed the surface; - * ``-ve`` number = the ray total internal reflected (TIR) at surface \ - given by the absolute value of the ``error`` - vigcode : list of integers - the first surface where the ray was vignetted. Unless an error occurs - at that surface or subsequent to that surface, the ray will continue - to trace to the requested surface. - pos : ndarray of shape (``numRays``,3) - local coordinates ``(x,y,z)`` of each ray on the requested surface - dir : ndarray of shape (``numRays``,3) - local direction cosines ``(l,m,n)`` after refraction into the media - following the requested surface. - normal : ndarray of shape (``numRays``,3) - local direction cosines ``(l2,m2,n2)`` of the surface normals at the - intersection point of the ray with the requested surface - opd : list of reals - computed optical path difference if ``want_opd <> 0`` - intensity : list of reals - the relative transmitted intensity of the ray, including any pupil - or surface apodization defined. - - If ray tracing fails, an RuntimeError is raised. - - Examples - -------- - >>> import numpy as np - >>> import matplotlib.pylab as plt - >>> # cartesian sampling in field an pupil - >>> x = np.linspace(-1,1,10) - >>> px= np.linspace(-1,1,3) - >>> grid = np.meshgrid(x,x,px,px); - >>> field= np.transpose(grid[0:2]).reshape(-1,2); - >>> pupil= np.transpose(grid[2:4]).reshape(-1,2); - >>> # run array-trace - >>> (error,vigcode,pos,dir,normal,opd,intensity) = \\ - >>> zGetTraceNumpy(field,pupil,mode=0); - >>> # plot results - >>> plt.scatter(pos[:,0],pos[:,1]) - - Notes - ----- - The opd can only be computed if the last surface is the image surface, - otherwise, the opd value will be zero. - """ - # handle input arguments - assert 2 == field.ndim == pupil.ndim, 'field and pupil should be 2d arrays' - assert field.shape == pupil.shape, 'we expect field and pupil points for each ray' - nRays = field.shape[0]; - if intensity is None: intensity=1; - if _np.isscalar(intensity): intensity=_np.zeros(nRays)+intensity; - if waveNum is None: waveNum=1; - if _np.isscalar(waveNum): waveNum=_np.zeros(nRays,dtype=_np.int)+waveNum; - - - # set up output arguments - error=_np.zeros(nRays,dtype=_np.int); - vigcode=_np.zeros(nRays,dtype=_np.int); - pos=_np.zeros((nRays,3)); - dir=_np.zeros((nRays,3)); - normal=_np.zeros((nRays,3)); - opd=_np.zeros(nRays); - - # numpyGetTrace(int nrays, double field[][2], double pupil[][2], - # double intensity[], int wave_num[], int mode, int surf, int want_opd, - # int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], - # double opd[], unsigned int timeout); - _numpyGetTrace = _array_trace_lib.numpyGetTrace - _numpyGetTrace.restype = _INT - _numpyGetTrace.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT,_INT, - _INT1D,_INT1D,_DBL2D,_DBL2D,_DBL2D,_DBL1D,_ct.c_uint] - ret = _numpyGetTrace(nRays,field,pupil,intensity,waveNum,mode,surf,want_opd, - error,vigcode,pos,dir,normal,opd,timeout) - # analyse error - flag - if ret==-1: raise RuntimeError("Couldn't retrieve data in PostArrayTraceMessage.") - if ret==-999: raise RuntimeError("Couldn't communicate with Zemax."); - if ret==-998: raise RuntimeError("Timeout reached after %dms"%timeout); - - return (error,vigcode,pos,dir,normal,opd,intensity); - def zGetTraceArray(numRays, hx=None, hy=None, px=None, py=None, intensity=None, waveNum=None, mode=0, surf=-1, want_opd=0, timeout=5000): """Trace large number of rays defined by their normalized field and pupil @@ -1103,73 +983,9 @@ def _test_arraytrace_module_basic(): print(rd[k].intensity) print("Success!") -def _test_zGetTraceNumpy(): - """very basic test for the zGetTraceNumpy function - """ - # Basic test of the module functions - print("Basic test of zGetTraceNumpy module:") - x = _np.linspace(-1,1,10) - px= _np.linspace(-1,1,3) - grid = _np.meshgrid(x,x,px,px); - field= _np.transpose(grid[0:2]).reshape(-1,2); - pupil= _np.transpose(grid[2:4]).reshape(-1,2); - - (error,vigcode,pos,dir,normal,opd,intensity) = \ - zGetTraceNumpy(field,pupil,mode=0); - - print(" number of rays: %d" % len(pos)); - if len(pos)<1e5: - import matplotlib.pylab as plt - from mpl_toolkits.mplot3d import Axes3D - fig = plt.figure() - ax = fig.add_subplot(111, projection='3d') - ax.scatter(*pos.T,c=opd);#_np.linalg.norm(pupil,axis=1)); - - print("Success!") - -def _test_zArrayTrace_vs_zGetTraceNumpy(): - """compare the two implementations against each other - """ - # Basic test of the module functions - print("Comparison of zArrayTrace and zGetTraceNumpy:") - nr = 441 - rd = getRayDataArray(nr) - # Fill the rest of the ray data array - pupil = 2*_np.random.rand(nr,2)-1; - field = 2*_np.random.rand(nr,2)-1; - for k in xrange(nr): - rd[k+1].x = field[k,0]; - rd[k+1].y = field[k,1]; - rd[k+1].z = pupil[k,0]; - rd[k+1].l = pupil[k,1]; - rd[k+1].intensity = 1.0; - rd[k+1].wave = 1; - rd[k+1].want_opd = 0 - # results of zArrayTrace - assert(zArrayTrace(rd)==0); - results = _np.asarray( [[r.error,r.vigcode,r.x,r.y,r.z,r.l,r.m,r.n,\ - r.Exr,r.Eyr,r.Ezr,r.opd,r.intensity] for r in rd[1:]] ); - # results of GetTraceArray - (error,vigcode,pos,dir,normal,opd,intensity) = \ - zGetTraceNumpy(field,pupil,mode=0); - # compare - def check(A,B): - isequal = _np.array_equal(A,B); - return "ok" if isequal else "failed" - print(" error : %s " % check(error,results[:,0])); - print(" vigcode : %s " % check(vigcode,results[:,1])); - print(" x,y,z : %s " % check(pos,results[:,2:5])); - print(" l,m,n : %s " % check(dir,results[:,5:8])); - print(" l2,m2,n2 : %s " % check(normal,results[:,8:11])); - print(" opd : %s " % check(opd,results[:,11])); - print(" intensity: %s " % check(intensity,results[:,12])); - - if __name__ == '__main__': # run the test functions _test_getRayDataArray() _test_arraytrace_module_basic() - _test_zGetTraceNumpy() - _test_zArrayTrace_vs_zGetTraceNumpy() \ No newline at end of file diff --git a/pyzdde/arraytrace/x64/Release/ArrayTrace.dll b/pyzdde/arraytrace/x64/Release/ArrayTrace.dll deleted file mode 100644 index 22ef481e572c455a985a6112181eb06e82bcb93a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90112 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 Date: Tue, 24 May 2016 18:40:42 +0200 Subject: [PATCH 06/23] improve documentation of numpy_interface.zGetTraceArray() --- pyzdde/arraytrace/numpy_interface.py | 33 ++++++++++++++++++---------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/pyzdde/arraytrace/numpy_interface.py b/pyzdde/arraytrace/numpy_interface.py index 7b62d39..4e49681 100644 --- a/pyzdde/arraytrace/numpy_interface.py +++ b/pyzdde/arraytrace/numpy_interface.py @@ -7,14 +7,15 @@ # This file is subject to the terms and conditions of the MIT License. # For further details, please refer to LICENSE.txt #------------------------------------------------------------------------------- -"""Module for doing array ray tracing as described in Zemax manual. The following -2 functions are provided according to 5 different modes discussed in the Zemax manual: +""" +Module for doing array ray tracing as described in Zemax manual. The following +functions are provided according to 5 different modes discussed in the Zemax manual: 1. zGetTraceArray() 2. zGetTraceDirectArray() - 3. zGetPolTraceArray() - 4. zGetPolTraceDirectArray() - 5. zGetNSCTraceArray() + ToDo: 3. zGetPolTraceArray() + ToDo: 4. zGetPolTraceDirectArray() + ToDo: 5. zGetNSCTraceArray() """ import os as _os import sys as _sys @@ -75,10 +76,9 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, Returns ------- error : list of integers - * ``0`` = ray traced successfully; - * ``+ve`` number = the ray missed the surface; - * ``-ve`` number = the ray total internal reflected (TIR) at surface \ - given by the absolute value of the ``error`` + * =0: ray traced successfully + * <0: ray missed the surface number indicated by ``error`` + * >0: total internal reflection of ray at the surface number given by ``-error`` vigcode : list of integers the first surface where the ray was vignetted. Unless an error occurs at that surface or subsequent to that surface, the ray will continue @@ -95,7 +95,8 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, computed optical path difference if ``want_opd <> 0`` intensity : list of reals the relative transmitted intensity of the ray, including any pupil - or surface apodization defined. + or surface apodization defined. Note mode 0 considers pupil apodization, + while mode 1 does not. If ray tracing fails, an RuntimeError is raised. @@ -118,7 +119,17 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, Notes ----- The opd can only be computed if the last surface is the image surface, - otherwise, the opd value will be zero. + otherwise, the opd value will be zero. It is not yet clear, if want_opd + works as described in the manual: + + If want_opd is less than zero(such as -1) then the both the chief ray and + specified ray are requested, and the OPD is the phase difference + between the two in waves of the current wavelength. If want_opd is + greater than zero, then the most recently traced chief ray data is used. + Therefore, the want_opd flag should be -1 whenever the chief ray changes; + and +1 for all subsequent rays which do not require the chief ray be + traced again. Generally the chief ray changes only when the field + coordinates or wavelength changes. """ # handle input arguments assert 2 == field.ndim == pupil.ndim, 'field and pupil should be 2d arrays' From 959a0e474e920a43fdbb8198b53d5a1ec02965e2 Mon Sep 17 00:00:00 2001 From: rhambach Date: Wed, 25 May 2016 16:37:50 +0200 Subject: [PATCH 07/23] correct BUG: pupil and field parameters in zGetTrace* was limited to 4 digits - results in differences between zGetTrace() and Zemax or zGetTraceArray() --- pyzdde/zdde.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyzdde/zdde.py b/pyzdde/zdde.py index 16e3866..3dab0ae 100644 --- a/pyzdde/zdde.py +++ b/pyzdde/zdde.py @@ -2523,10 +2523,10 @@ def zGetPolTrace(self, waveNum, mode, surf, hx, hy, px, py, Ex, Ey, Phax, Phay): zGetPolTraceDirect(), zGetTrace(), zGetTraceDirect() """ args1 = "{wN:d},{m:d},{s:d},".format(wN=waveNum,m=mode,s=surf) - args2 = "{hx:1.4f},{hy:1.4f},".format(hx=hx,hy=hy) - args3 = "{px:1.4f},{py:1.4f},".format(px=px,py=py) - args4 = "{Ex:1.4f},{Ey:1.4f},".format(Ex=Ex,Ey=Ey) - args5 = "{Phax:1.4f},{Phay:1.4f}".format(Phax=Phax,Phay=Phay) + args2 = "{hx:1.20g},{hy:1.20g},".format(hx=hx,hy=hy) + args3 = "{px:1.20g},{py:1.20g},".format(px=px,py=py) + args4 = "{Ex:1.20g},{Ey:1.20g},".format(Ex=Ex,Ey=Ey) + args5 = "{Phax:1.20g},{Phay:1.20g}".format(Phax=Phax,Phay=Phay) cmd = "GetPolTrace," + args1 + args2 + args3 + args4 + args5 reply = self._sendDDEcommand(cmd) rs = reply.split(',') @@ -2605,8 +2605,8 @@ def zGetPolTraceDirect(self, waveNum, mode, startSurf, stopSurf, args1 = "{sa:d},{sd:d},".format(sa=startSurf,sd=stopSurf) args2 = "{x:1.20g},{y:1.20g},{z:1.20g},".format(x=x,y=y,z=z) args3 = "{l:1.20g},{m:1.20g},{n:1.20g},".format(l=l,m=m,n=n) - args4 = "{Ex:1.4f},{Ey:1.4f},".format(Ex=Ex,Ey=Ey) - args5 = "{Phax:1.4f},{Phay:1.4f}".format(Phax=Phax,Phay=Phay) + args4 = "{Ex:1.20g},{Ey:1.20g},".format(Ex=Ex,Ey=Ey) + args5 = "{Phax:1.20g},{Phay:1.20g}".format(Phax=Phax,Phay=Phay) cmd = ("GetPolTraceDirect," + args0 + args1 + args2 + args3 + args4 + args5) reply = self._sendDDEcommand(cmd) @@ -3393,8 +3393,8 @@ def zGetTrace(self, waveNum, mode, surf, hx, hy, px, py): zGetPolTraceDirect() """ args1 = "{wN:d},{m:d},{s:d},".format(wN=waveNum,m=mode,s=surf) - args2 = "{hx:1.4f},{hy:1.4f},".format(hx=hx,hy=hy) - args3 = "{px:1.4f},{py:1.4f}".format(px=px,py=py) + args2 = "{hx:1.20g},{hy:1.20g},".format(hx=hx,hy=hy) + args3 = "{px:1.20g},{py:1.20g}".format(px=px,py=py) cmd = "GetTrace," + args1 + args2 + args3 reply = self._sendDDEcommand(cmd) rs = reply.split(',') @@ -3467,8 +3467,8 @@ def zGetTraceDirect(self, waveNum, mode, startSurf, stopSurf, x, y, z, l, m, n): """ args1 = "{wN:d},{m:d},".format(wN=waveNum,m=mode) args2 = "{sa:d},{sp:d},".format(sa=startSurf,sp=stopSurf) - args3 = "{x:1.20f},{y:1.20f},{z:1.20f},".format(x=x,y=y,z=z) - args4 = "{l:1.20f},{m:1.20f},{n:1.20f}".format(l=l,m=m,n=n) + args3 = "{x:1.20g},{y:1.20g},{z:1.20g},".format(x=x,y=y,z=z) + args4 = "{l:1.20g},{m:1.20g},{n:1.20g}".format(l=l,m=m,n=n) cmd = "GetTraceDirect," + args1 + args2 + args3 + args4 reply = self._sendDDEcommand(cmd) rs = reply.split(',') From 948c34afaafe166e690e940ae1da250b90ef4f4c Mon Sep 17 00:00:00 2001 From: rhambach Date: Wed, 25 May 2016 16:43:03 +0200 Subject: [PATCH 08/23] promote want_opd to array and initialize all arrays with 0 --- pyzdde/arraytrace/arrayTraceClient.c | 21 ++++++---- pyzdde/arraytrace/arrayTraceClient.h | 4 +- pyzdde/arraytrace/numpy_interface.py | 62 ++++++++++++++++------------ 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/pyzdde/arraytrace/arrayTraceClient.c b/pyzdde/arraytrace/arrayTraceClient.c index e8c6a48..84ef00e 100644 --- a/pyzdde/arraytrace/arrayTraceClient.c +++ b/pyzdde/arraytrace/arrayTraceClient.c @@ -88,7 +88,14 @@ int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) return RETVAL; } -int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], double intensity[], int wave_num[], int mode, int surf, int want_opd, +/* ---------------------------------------------------------------------------------- + ArrayTrace functions that accespt Numpy arrays as arguments + avoids large overhead times in Python wrapper functions + ---------------------------------------------------------------------------------- + */ + +// Mode 0: similar to GetTrace (rays defined by field and pupil coordinates) +int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], double intensity[], int wave_num[], int mode, int surf, int want_opd[], int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], double opd[], unsigned int timeout) { int i; @@ -96,8 +103,8 @@ int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], dou // http://scipy.github.io/old-wiki/pages/Cookbook/Ctypes#NumPy.27s_ndpointer_with_ctypes_argtypes // allocate memory for list of structures expected by ZEMAX - DDERAYDATA* RD = malloc((nrays+1) * sizeof(*RD)); - + DDERAYDATA* RD = calloc(nrays+1,sizeof(*RD)); + // set parameters for raytrace (in 0'th element) // see Zemax manual for meaning of the fields (do not infer from field names!) RD[0].opd=0; // set type 0 (GetTrace) @@ -116,7 +123,7 @@ int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], dou RD[i+1].wave = wave_num[i]; RD[i+1].error= 0; RD[i+1].vigcode=0; - RD[i+1].want_opd=want_opd; + RD[i+1].want_opd=want_opd[i]; } // arrayTrace @@ -124,8 +131,6 @@ int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], dou // was successful, fill return values for (i=0; i 0`` + computed optical path difference in waves of the current wavelength, + only computed, if ``want_opd <> 0`` and ``surf=-1`` intensity : list of reals the relative transmitted intensity of the ray, including any pupil or surface apodization defined. Note mode 0 considers pupil apodization, @@ -112,19 +116,19 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, >>> pupil= np.transpose(grid[2:4]).reshape(-1,2); >>> # run array-trace >>> (error,vigcode,pos,dir,normal,opd,intensity) = \\ - >>> zGetTraceNumpy(field,pupil,mode=0); + >>> zGetTraceArray(field,pupil); >>> # plot results >>> plt.scatter(pos[:,0],pos[:,1]) Notes ----- The opd can only be computed if the last surface is the image surface, - otherwise, the opd value will be zero. It is not yet clear, if want_opd - works as described in the manual: + otherwise, the opd value will be zero. The meaning of want_opd is explained + in the Zemax manual: If want_opd is less than zero(such as -1) then the both the chief ray and - specified ray are requested, and the OPD is the phase difference - between the two in waves of the current wavelength. If want_opd is + specified ray are requested, and the OPD is the phase difference + between the two in waves of the current wavelength. If want_opd is greater than zero, then the most recently traced chief ray data is used. Therefore, the want_opd flag should be -1 whenever the chief ray changes; and +1 for all subsequent rays which do not require the chief ray be @@ -132,15 +136,18 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, coordinates or wavelength changes. """ # handle input arguments - assert 2 == field.ndim == pupil.ndim, 'field and pupil should be 2d arrays' - assert field.shape == pupil.shape, 'we expect field and pupil points for each ray' nRays = field.shape[0]; + assert (nRays,2) == field.shape == pupil.shape, 'field and pupil should have shape (nRays,2)' if intensity is None: intensity=1; - if _np.isscalar(intensity): intensity=_np.zeros(nRays)+intensity; + if _np.isscalar(intensity): intensity=_np.full(nRays,intensity,dtype=_np.double); if waveNum is None: waveNum=1; - if _np.isscalar(waveNum): waveNum=_np.zeros(nRays,dtype=_np.int)+waveNum; - - + if _np.isscalar(waveNum): waveNum=_np.full(nRays,waveNum,dtype=_np.int); + if surf<>-1: want_opd=0; + if _np.isscalar(want_opd):want_opd=_np.full(nRays,want_opd,dtype=_np.int); + assert intensity.shape == (nRays,), 'intensity must be scalar or a vector of length nRays' + assert waveNum.shape == (nRays,), 'waveNum must be scalar or a vector of length nRays' + assert want_opd.shape == (nRays,), 'want_opd must be scalar or a vector of length nRays' + # set up output arguments error=_np.zeros(nRays,dtype=_np.int); vigcode=_np.zeros(nRays,dtype=_np.int); @@ -150,19 +157,21 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, opd=_np.zeros(nRays); # numpyGetTrace(int nrays, double field[][2], double pupil[][2], - # double intensity[], int wave_num[], int mode, int surf, int want_opd, + # double intensity[], int wave_num[], int mode, int surf, int want_opd[], # int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], # double opd[], unsigned int timeout); _numpyGetTrace = _array_trace_lib.numpyGetTrace _numpyGetTrace.restype = _INT - _numpyGetTrace.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT,_INT, + _numpyGetTrace.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT,_INT1D, _INT1D,_INT1D,_DBL2D,_DBL2D,_DBL2D,_DBL1D,_ct.c_uint] - ret = _numpyGetTrace(nRays,field,pupil,intensity,waveNum,mode,surf,want_opd, + ret = _numpyGetTrace(nRays,field,pupil,intensity,waveNum,int(bParaxial),surf,want_opd, error,vigcode,pos,dir,normal,opd,timeout) # analyse error - flag if ret==-1: raise RuntimeError("Couldn't retrieve data in PostArrayTraceMessage.") if ret==-999: raise RuntimeError("Couldn't communicate with Zemax."); if ret==-998: raise RuntimeError("Timeout reached after %dms"%timeout); + # set opd to NaN, where it was not calculated to avoid confusion + opd[want_opd==0]=_np.nan; return (error,vigcode,pos,dir,normal,opd,intensity); @@ -179,23 +188,22 @@ def _test_zGetTraceArray(): """ # Basic test of the module functions print("Basic test of zGetTraceNumpy module:") - x = _np.linspace(-1,1,10) + x = _np.linspace(-1,1,4) px= _np.linspace(-1,1,3) grid = _np.meshgrid(x,x,px,px); field= _np.transpose(grid[0:2]).reshape(-1,2); pupil= _np.transpose(grid[2:4]).reshape(-1,2); (error,vigcode,pos,dir,normal,opd,intensity) = \ - zGetTraceArray(field,pupil,mode=0); + zGetTraceArray(field,pupil,bParaxial=False,want_opd=1,surf=-1); print(" number of rays: %d" % len(pos)); if len(pos)<1e5: import matplotlib.pylab as plt from mpl_toolkits.mplot3d import Axes3D fig = plt.figure() - ax = fig.add_subplot(111, projection='3d') - ax.scatter(*pos.T,c=opd);#_np.linalg.norm(pupil,axis=1)); - + ax = fig.add_subplot(111,projection='3d') + ax.scatter(*pos.T,c=opd); print("Success!") From 9c8b55bb9790b0406a8e8465798a9c114ebed1d3 Mon Sep 17 00:00:00 2001 From: rhambach Date: Wed, 25 May 2016 17:12:16 +0200 Subject: [PATCH 09/23] fix BUG: document unexpected behavior for want_opd - see added notes - we have to ensure, that chief ray is requested at least at the first ray for which we want to calculate the OPD. Otherwise we get random results (not just an offset by an unknown value!). --- pyzdde/arraytrace/numpy_interface.py | 23 +++++++++++++++++++++- pyzdde/arraytrace/raystruct_interface.py | 25 ++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/pyzdde/arraytrace/numpy_interface.py b/pyzdde/arraytrace/numpy_interface.py index 31bf22b..be77bc5 100644 --- a/pyzdde/arraytrace/numpy_interface.py +++ b/pyzdde/arraytrace/numpy_interface.py @@ -17,6 +17,22 @@ ToDo: 3. zGetPolTraceArray() ToDo: 4. zGetPolTraceDirectArray() ToDo: 5. zGetNSCTraceArray() + +Note +----- + +The parameter want_opd is very confusing as it alters the behavior of GetTraceArray. +If the calculation of OPD is requested for a ray, +- vigcode becomes a different meaning (seems to be 1 if vignetted, but no longer related to surface) +- bParaxial (mode) becomes inactive, Zemax always performs a real-raytrace ! +- the surface normal is not calculated +- if the calculation of the chief ray data is not requested for the first ray, + e.g. by setting all want_opd to 1, wrong OPD values are returned (without any relation to the real values) +- this affects only rays with want_opd<>0 -> i.e. if it is mixed, one obtains a total mess + + +The pupil apodization seems to be always considered independent of the mode / bParaxial value +in contrast to the note in the Zemax Manual (tested with Zemax 13 Release 2 SP 5 Premium 64bit) """ import os as _os import sys as _sys @@ -109,7 +125,7 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, >>> import numpy as np >>> import matplotlib.pylab as plt >>> # cartesian sampling in field an pupil - >>> x = np.linspace(-1,1,10) + >>> x = np.linspace(-1,1,4) >>> px= np.linspace(-1,1,3) >>> grid = np.meshgrid(x,x,px,px); >>> field= np.transpose(grid[0:2]).reshape(-1,2); @@ -148,6 +164,11 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, assert waveNum.shape == (nRays,), 'waveNum must be scalar or a vector of length nRays' assert want_opd.shape == (nRays,), 'want_opd must be scalar or a vector of length nRays' + # if opd is requested, make sure that chief ray data is initialized at first requested ray + if _np.any(want_opd<>0): + first_ray = _np.where(want_opd<>0)[0][0]; + want_opd[first_ray] = -1; + # set up output arguments error=_np.zeros(nRays,dtype=_np.int); vigcode=_np.zeros(nRays,dtype=_np.int); diff --git a/pyzdde/arraytrace/raystruct_interface.py b/pyzdde/arraytrace/raystruct_interface.py index 3a099f8..79ce87c 100644 --- a/pyzdde/arraytrace/raystruct_interface.py +++ b/pyzdde/arraytrace/raystruct_interface.py @@ -23,6 +23,23 @@ 3. zGetPolTraceArray() 4. zGetPolTraceDirectArray() 5. zGetNSCTraceArray() + +Note +----- + +The parameter want_opd is very confusing as it alters the behavior of GetTraceArray. +If the calculation of OPD is requested for a ray, +- vigcode becomes a different meaning (seems to be 1 if vignetted, but no longer related to surface) +- bParaxial (mode) becomes inactive, Zemax always performs a real-raytrace ! +- the surface normal is not calculated +- if the calculation of the chief ray data is not requested for the first ray, + e.g. by setting all want_opd to 1, wrong OPD values are returned (without any relation to the real values) +- this affects only rays with want_opd<>0 -> i.e. if it is mixed, one obtains a total mess + + +The pupil apodization seems to be always considered independent of the mode / bParaxial value +in contrast to the note in the Zemax Manual (tested with Zemax 13 Release 2 SP 5 Premium 64bit) + """ from __future__ import print_function import os as _os @@ -254,7 +271,11 @@ def zGetTraceArray(numRays, hx=None, hy=None, px=None, py=None, intensity=None, waveNum = waveNum if isinstance(waveNum, list) else [waveNum]*numRays else: waveNum = [1] * numRays - want_opd = [want_opd] * numRays + + # if opd is requested, make sure that chief ray data is initialized at first ray + if want_opd==0: want_opd = [0] * numRays; + else: want_opd = [-1] + [want_opd] * (numRays-1); + # fill up the structure for i in xrange(1, numRays+1): rd[i].x = hx[i-1] @@ -264,7 +285,7 @@ def zGetTraceArray(numRays, hx=None, hy=None, px=None, py=None, intensity=None, rd[i].intensity = intensity[i-1] rd[i].wave = waveNum[i-1] rd[i].want_opd = want_opd[i-1] - + # call ray tracing ret = zArrayTrace(rd, timeout) From e902c4017f31c8656856d87a32e87c9e10b8503f Mon Sep 17 00:00:00 2001 From: rhambach Date: Wed, 25 May 2016 17:15:17 +0200 Subject: [PATCH 10/23] correct unittest for ArrayTrace according to previous changes, works - add direct comparison between ArrayTrace and Single Trace --- test/arrayTraceTest.py | 116 +++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 57 deletions(-) diff --git a/test/arrayTraceTest.py b/test/arrayTraceTest.py index f94f523..731d3ba 100644 --- a/test/arrayTraceTest.py +++ b/test/arrayTraceTest.py @@ -2,6 +2,8 @@ #------------------------------------------------------------------------------- # Name: ArrayTraceUnittest.py # Purpose: unit tests for ArrayTrace +# Run: from home directory of the project, i.e., +# $ python -m unittest test.arrayTraceTest # # Licence: MIT License # This file is subject to the terms and conditions of the MIT License. @@ -14,7 +16,7 @@ import unittest import numpy as np -import pyzdde.zdde as pyzdde +import pyzdde.zdde as pyz import pyzdde.arraytrace as at import pyzdde.arraytrace.numpy_interface as nt @@ -25,7 +27,7 @@ class TestArrayTrace(unittest.TestCase): @classmethod def setUpClass(self): print('RUNNING TESTS FOR MODULE \'%s\'.'% at.__file__) - self.ln = pyzdde.createLink(); + self.ln = pyz.createLink(); self.ln.zGetUpdate(); @classmethod @@ -64,73 +66,73 @@ def test_getRayDataArray(self): self.assertEqual(rd[0].z,0.0) self.assertEqual(rd[0].error,1) - def test_zArrayTrace(self): - print("\nTEST: arraytrace.zArrayTrace()") + def test_zGetTraceArray(self): + print("\nTEST: arraytrace.zGetTraceArray()") # Load a lens file into the Lde filename = get_test_file() self.ln.zLoadFile(filename) self.ln.zPushLens(1); - # set up field and pupil sampling - nr = 9 - rd = at.getRayDataArray(nr) - k = 0 - for i in xrange(-10, 11, 10): - for j in xrange(-10, 11, 10): - k += 1 - rd[k].z = i/20.0 # px - rd[k].l = j/20.0 # py - rd[k].intensity = 1.0 - rd[k].wave = 1 - rd[k].want_opd = 0 - # run array trace (C-extension) - ret = at.zArrayTrace(rd) - self.assertEqual(ret,0); - #r = rd[1]; # select first ray - #for key in ('error','vigcode','x','y','z','l','m','n','Exr','Eyr','Ezr','opd','intensity'): - # print('self.assertAlmostEqual(rd[1].%s,%.8g,msg=\'%s differs\');'%(key,getattr(rd[1],key),key)); - self.assertAlmostEqual(rd[1].error,0,msg='error differs'); - self.assertAlmostEqual(rd[1].vigcode,0,msg='vigcode differs'); - self.assertAlmostEqual(rd[1].x,-0.0029856861,msg='x differs'); - self.assertAlmostEqual(rd[1].y,-0.0029856861,msg='y differs'); - self.assertAlmostEqual(rd[1].z,0,msg='z differs'); - self.assertAlmostEqual(rd[1].l,0.050136296,msg='l differs'); - self.assertAlmostEqual(rd[1].m,0.050136296,msg='m differs'); - self.assertAlmostEqual(rd[1].n,0.99748318,msg='n differs'); - self.assertAlmostEqual(rd[1].Exr,0,msg='Exr differs'); - self.assertAlmostEqual(rd[1].Eyr,0,msg='Eyr differs'); - self.assertAlmostEqual(rd[1].Ezr,-1,msg='Ezr differs'); - self.assertAlmostEqual(rd[1].opd,64.711234,msg='opd differs',places=5); - self.assertAlmostEqual(rd[1].intensity,1,msg='intensity differs'); + # set up field and pupil sampling with random values + nr = 22; np.random.seed(0); + hx,hy,px,py = 2*np.random.rand(4,nr)-1; + w = 1; # wavenum + + # run array trace (C-extension), returns (error,vig,x,y,z,l,m,n,l2,m2,n2,opd,intensity) + mode_descr = ("real","paraxial") + for mode in (0,1): + print(" compare with GetTrace for %s raytrace"% mode_descr[mode]); + ret = at.zGetTraceArray(nr,list(hx),list(hy),list(px),list(py), + intensity=1,waveNum=w,mode=mode,surf=-1,want_opd=0) + mask = np.ones(13,dtype=np.bool); mask[-2]=False; # mask array for removing opd from ret + ret = np.asarray(ret)[mask]; + + # compare with results from GetTrace, returns (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) + ret_descr = ('error','vigcode','x','y','z','l','m','n','Exr','Eyr','Ezr','intensity') + for i in xrange(nr): + reference = self.ln.zGetTrace(w,mode,-1,hx[i],hy[i],px[i],py[i]); + is_close = np.isclose(ret[:,i], np.asarray(reference)); + msg = 'zGetTraceArray differs from GetTrace for %s ray #%d:\n' % (mode_descr[mode],i); + msg+= ' field: (%f,%f), pupil: (%f,%f) \n' % (hx[i],hy[i],px[i],py[i]); + msg+= ' parameter zGetTraceArray zGetTrace \n'; + for j in np.arange(12)[~is_close]: + msg+= '%10s %12g %12g \n'%(ret_descr[j],ret[j,i],reference[j]); + self.assertTrue(np.all(is_close), msg=msg); def test_zGetTraceNumpy(self): - print("\nTEST: arraytrace.zGetTraceNumpy()") + print("\nTEST: arraytrace.numpy_interface.zGetTraceArray()") # Load a lens file into the LDE filename = get_test_file() self.ln.zLoadFile(filename) self.ln.zPushLens(1); # set-up field and pupil sampling - x = np.linspace(-1,1,3) - px= np.linspace(-1,1,3) - grid = np.meshgrid(x,x,px,px); - field= np.transpose(grid[0:2]).reshape(-1,2); - pupil= np.transpose(grid[2:4]).reshape(-1,2); - # array trace (C-extension) - ret = nt.zGetTraceArray(field,pupil,mode=0); - self.assertEqual(len(field),3**4); - - #for i in xrange(len(ret)): - # name = ['error','vigcode','pos','dir','normal','opd','intensity'] - # print('self.assertAlmostEqual(ret[%d][1],%s,msg=\'%s differs\');'%(i,str(ret[i][1]),name[i])); - self.assertEqual(ret[0][1],0,msg='error differs'); - self.assertEqual(ret[1][1],3,msg='vigcode differs'); - self.assertTrue(np.allclose(ret[2][1],[-18.24210131, -0.0671553, 0.]),msg='pos differs'); - self.assertTrue(np.allclose(ret[3][1],[-0.24287826, 0.09285061, 0.96560288]),msg='dir differs'); - self.assertTrue(np.allclose(ret[4][1],[ 0, 0, -1]),msg='normal differs'); - self.assertAlmostEqual(ret[5][1],66.8437599679,msg='opd differs'); - self.assertAlmostEqual(ret[6][1],1.0,msg='intensity differs'); + nr = 22; np.random.seed(0); + field = 2*np.random.rand(nr,2)-1; hx,hy = field.T; + pupil = 2*np.random.rand(nr,2)-1; px,py = pupil.T; + w = 1; # wavenum + # run array trace (C-extension), returns (error,vigcode,pos,dir,normal,opd,intensity) + mode_descr = ("real","paraxial") + for mode in (0,1): + print(" compare with GetTrace for %s raytrace"% mode_descr[mode]); + ret = nt.zGetTraceArray(field,pupil,bParaxial=(mode==1),waveNum=w,surf=-1); + mask = np.ones(13,dtype=np.bool); mask[-2]=False; # mask array for removing opd from ret + ret = np.column_stack(ret).T; + ret = ret[mask]; + + # compare with results from GetTrace, returns (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) + ret_descr = ('error','vigcode','x','y','z','l','m','n','Exr','Eyr','Ezr','intensity') + for i in xrange(nr): + reference = self.ln.zGetTrace(w,mode,-1,hx[i],hy[i],px[i],py[i]); + is_close = np.isclose(ret[:,i], np.asarray(reference)); + msg = 'zGetTraceArray differs from GetTrace for %s ray #%d:\n' % (mode_descr[mode],i); + msg+= ' field: (%f,%f), pupil: (%f,%f) \n' % (hx[i],hy[i],px[i],py[i]); + msg+= ' parameter zGetTraceArray zGetTrace \n'; + for j in np.arange(12)[~is_close]: + msg+= '%10s %12g %12g \n'%(ret_descr[j],ret[j,i],reference[j]); + self.assertTrue(np.all(is_close), msg=msg); + def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): print("\nTEST: comparison of zArrayTrace and zGetTraceNumpy") @@ -150,7 +152,7 @@ def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): rd[k+1].l = pupil[k,1]; rd[k+1].intensity = 1.0; rd[k+1].wave = 1; - rd[k+1].want_opd = 0 + rd[k+1].want_opd = -1; # results of zArrayTrace ret = at.zArrayTrace(rd); self.assertEqual(ret,0); @@ -158,7 +160,7 @@ def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): r.Exr,r.Eyr,r.Ezr,r.opd,r.intensity] for r in rd[1:]] ); # results of GetTraceArray (error,vigcode,pos,dir,normal,opd,intensity) = \ - nt.zGetTraceArray(field,pupil,mode=0); + nt.zGetTraceArray(field,pupil,bParaxial=0,want_opd=-1); # compare self.assertTrue(np.array_equal(error,results[:,0]),msg="error differs"); From ec48f05357527236fc33c960ba5419c4f0c6a505 Mon Sep 17 00:00:00 2001 From: rhambach Date: Wed, 25 May 2016 20:29:03 +0200 Subject: [PATCH 11/23] remove OPD calculation from numpy_interface.zGetArrayTrace() - ToDo: reintroduce OPD calculation in separate function --- pyzdde/arraytrace/arrayTraceClient.c | 12 +++--- pyzdde/arraytrace/arrayTraceClient.h | 7 ++-- pyzdde/arraytrace/numpy_interface.py | 62 +++++++--------------------- test/arrayTraceTest.py | 11 ++--- 4 files changed, 27 insertions(+), 65 deletions(-) diff --git a/pyzdde/arraytrace/arrayTraceClient.c b/pyzdde/arraytrace/arrayTraceClient.c index 84ef00e..a354305 100644 --- a/pyzdde/arraytrace/arrayTraceClient.c +++ b/pyzdde/arraytrace/arrayTraceClient.c @@ -95,8 +95,9 @@ int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) */ // Mode 0: similar to GetTrace (rays defined by field and pupil coordinates) -int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], double intensity[], int wave_num[], int mode, int surf, int want_opd[], - int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], double opd[], unsigned int timeout) +int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], + double intensity[], int wave_num[], int mode, int surf, int error[], int vigcode[], + double pos[][3], double dir[][3], double normal[][3], unsigned int timeout) { int i; // how to call this function ? @@ -121,9 +122,9 @@ int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], dou RD[i+1].l = pupil[i][1]; RD[i+1].intensity = intensity[i]; RD[i+1].wave = wave_num[i]; - RD[i+1].error= 0; - RD[i+1].vigcode=0; - RD[i+1].want_opd=want_opd[i]; + //RD[i+1].error= 0; // already initialized to 0 + //RD[i+1].vigcode=0; + //RD[i+1].want_opd=0; } // arrayTrace @@ -140,7 +141,6 @@ int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], dou normal[i][0]=RD[i+1].Exr; normal[i][1]=RD[i+1].Eyr; normal[i][2]=RD[i+1].Ezr; - opd[i] =RD[i+1].opd; intensity[i]=RD[i+1].intensity; error[i] =RD[i+1].error; vigcode[i] =RD[i+1].vigcode; diff --git a/pyzdde/arraytrace/arrayTraceClient.h b/pyzdde/arraytrace/arrayTraceClient.h index 3075775..27e0773 100644 --- a/pyzdde/arraytrace/arrayTraceClient.h +++ b/pyzdde/arraytrace/arrayTraceClient.h @@ -32,10 +32,9 @@ void rayTraceFunction(); // general arrayTrace function accepting DDERAYDATA structure DLL_EXPORT int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout); // wrapper for numpy arrays: mode 0 -DLL_EXPORT int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], - double intensity[], int wave_num[], int mode, int surf, int want_opd[], - int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], - double opd[], unsigned int timeout); +DLL_EXPORT int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], + double intensity[], int wave_num[], int mode, int surf, int error[], int vigcode[], + double pos[][3], double dir[][3], double normal[][3], unsigned int timeout); #ifdef __cplusplus } diff --git a/pyzdde/arraytrace/numpy_interface.py b/pyzdde/arraytrace/numpy_interface.py index be77bc5..dce2f3f 100644 --- a/pyzdde/arraytrace/numpy_interface.py +++ b/pyzdde/arraytrace/numpy_interface.py @@ -61,7 +61,7 @@ def _is64bit(): _DBL2D = _np.ctypeslib.ndpointer(ndim=2,dtype=_np.double,flags=["C_CONTIGUOUS","ALIGNED"]) def zGetTraceArray(field, pupil, intensity=None, waveNum=None, - bParaxial=False, surf=-1, want_opd=0, timeout=60000): + bParaxial=False, surf=-1, timeout=60000): """Trace large number of rays defined by their normalized field and pupil coordinates on lens file in the LDE of main Zemax application (not in the DDE server) @@ -84,11 +84,6 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, surf : integer, optional surface to trace the ray to. Usually, the ray data is only needed at the image surface (``surf = -1``, default) - want_opd : integer or vector of length ``numRays``, optional, requires ``surf==-1`` - determines (for each ray), if optical path difference (OPD) is calculated - * 0: OPD is not calculated (Default), - *-1: OPD between ray and corresponding chief-ray is calculated (doubled time!) - * 1: OPD between ray and last chief-ray is calculated. See Notes below. timeout : integer, optional command timeout specified in milli-seconds (default: 1min), at least 1s @@ -110,9 +105,6 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, normal : ndarray of shape (``numRays``,3) local direction cosines ``(l2,m2,n2)`` of the surface normals at the intersection point of the ray with the requested surface - opd : list of reals - computed optical path difference in waves of the current wavelength, - only computed, if ``want_opd <> 0`` and ``surf=-1`` intensity : list of reals the relative transmitted intensity of the ray, including any pupil or surface apodization defined. Note mode 0 considers pupil apodization, @@ -131,25 +123,10 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, >>> field= np.transpose(grid[0:2]).reshape(-1,2); >>> pupil= np.transpose(grid[2:4]).reshape(-1,2); >>> # run array-trace - >>> (error,vigcode,pos,dir,normal,opd,intensity) = \\ + >>> (error,vigcode,pos,dir,normal,intensity) = \\ >>> zGetTraceArray(field,pupil); >>> # plot results >>> plt.scatter(pos[:,0],pos[:,1]) - - Notes - ----- - The opd can only be computed if the last surface is the image surface, - otherwise, the opd value will be zero. The meaning of want_opd is explained - in the Zemax manual: - - If want_opd is less than zero(such as -1) then the both the chief ray and - specified ray are requested, and the OPD is the phase difference - between the two in waves of the current wavelength. If want_opd is - greater than zero, then the most recently traced chief ray data is used. - Therefore, the want_opd flag should be -1 whenever the chief ray changes; - and +1 for all subsequent rays which do not require the chief ray be - traced again. Generally the chief ray changes only when the field - coordinates or wavelength changes. """ # handle input arguments nRays = field.shape[0]; @@ -158,16 +135,8 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, if _np.isscalar(intensity): intensity=_np.full(nRays,intensity,dtype=_np.double); if waveNum is None: waveNum=1; if _np.isscalar(waveNum): waveNum=_np.full(nRays,waveNum,dtype=_np.int); - if surf<>-1: want_opd=0; - if _np.isscalar(want_opd):want_opd=_np.full(nRays,want_opd,dtype=_np.int); assert intensity.shape == (nRays,), 'intensity must be scalar or a vector of length nRays' assert waveNum.shape == (nRays,), 'waveNum must be scalar or a vector of length nRays' - assert want_opd.shape == (nRays,), 'want_opd must be scalar or a vector of length nRays' - - # if opd is requested, make sure that chief ray data is initialized at first requested ray - if _np.any(want_opd<>0): - first_ray = _np.where(want_opd<>0)[0][0]; - want_opd[first_ray] = -1; # set up output arguments error=_np.zeros(nRays,dtype=_np.int); @@ -175,26 +144,23 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, pos=_np.zeros((nRays,3)); dir=_np.zeros((nRays,3)); normal=_np.zeros((nRays,3)); - opd=_np.zeros(nRays); - # numpyGetTrace(int nrays, double field[][2], double pupil[][2], - # double intensity[], int wave_num[], int mode, int surf, int want_opd[], - # int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], - # double opd[], unsigned int timeout); + # numpyGetTrace(int nrays, double field[][2], double pupil[][2], double intensity[], + # int wave_num[], int mode, int surf, int error[], int vigcode[], double pos[][3], + # double dir[][3], double normal[][3], unsigned int timeout); _numpyGetTrace = _array_trace_lib.numpyGetTrace _numpyGetTrace.restype = _INT - _numpyGetTrace.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT,_INT1D, - _INT1D,_INT1D,_DBL2D,_DBL2D,_DBL2D,_DBL1D,_ct.c_uint] - ret = _numpyGetTrace(nRays,field,pupil,intensity,waveNum,int(bParaxial),surf,want_opd, - error,vigcode,pos,dir,normal,opd,timeout) + _numpyGetTrace.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT, + _INT1D,_INT1D,_DBL2D,_DBL2D,_DBL2D,_ct.c_uint] + ret = _numpyGetTrace(nRays,field,pupil,intensity,waveNum,int(bParaxial),surf, + error,vigcode,pos,dir,normal,timeout) # analyse error - flag if ret==-1: raise RuntimeError("Couldn't retrieve data in PostArrayTraceMessage.") if ret==-999: raise RuntimeError("Couldn't communicate with Zemax."); if ret==-998: raise RuntimeError("Timeout reached after %dms"%timeout); - # set opd to NaN, where it was not calculated to avoid confusion - opd[want_opd==0]=_np.nan; - return (error,vigcode,pos,dir,normal,opd,intensity); + return (error,vigcode,pos,dir,normal,intensity); + @@ -215,8 +181,8 @@ def _test_zGetTraceArray(): field= _np.transpose(grid[0:2]).reshape(-1,2); pupil= _np.transpose(grid[2:4]).reshape(-1,2); - (error,vigcode,pos,dir,normal,opd,intensity) = \ - zGetTraceArray(field,pupil,bParaxial=False,want_opd=1,surf=-1); + (error,vigcode,pos,dir,normal,intensity) = \ + zGetTraceArray(field,pupil,bParaxial=False,surf=-1); print(" number of rays: %d" % len(pos)); if len(pos)<1e5: @@ -224,7 +190,7 @@ def _test_zGetTraceArray(): from mpl_toolkits.mplot3d import Axes3D fig = plt.figure() ax = fig.add_subplot(111,projection='3d') - ax.scatter(*pos.T,c=opd); + ax.scatter(*pos.T,c=_np.linalg.norm(pupil,axis=1)); print("Success!") diff --git a/test/arrayTraceTest.py b/test/arrayTraceTest.py index 731d3ba..622862c 100644 --- a/test/arrayTraceTest.py +++ b/test/arrayTraceTest.py @@ -117,10 +117,8 @@ def test_zGetTraceNumpy(self): for mode in (0,1): print(" compare with GetTrace for %s raytrace"% mode_descr[mode]); ret = nt.zGetTraceArray(field,pupil,bParaxial=(mode==1),waveNum=w,surf=-1); - mask = np.ones(13,dtype=np.bool); mask[-2]=False; # mask array for removing opd from ret ret = np.column_stack(ret).T; - ret = ret[mask]; - + # compare with results from GetTrace, returns (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) ret_descr = ('error','vigcode','x','y','z','l','m','n','Exr','Eyr','Ezr','intensity') for i in xrange(nr): @@ -152,15 +150,15 @@ def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): rd[k+1].l = pupil[k,1]; rd[k+1].intensity = 1.0; rd[k+1].wave = 1; - rd[k+1].want_opd = -1; + rd[k+1].want_opd = 0; # results of zArrayTrace ret = at.zArrayTrace(rd); self.assertEqual(ret,0); results = np.asarray( [[r.error,r.vigcode,r.x,r.y,r.z,r.l,r.m,r.n,\ r.Exr,r.Eyr,r.Ezr,r.opd,r.intensity] for r in rd[1:]] ); # results of GetTraceArray - (error,vigcode,pos,dir,normal,opd,intensity) = \ - nt.zGetTraceArray(field,pupil,bParaxial=0,want_opd=-1); + (error,vigcode,pos,dir,normal,intensity) = \ + nt.zGetTraceArray(field,pupil,bParaxial=0); # compare self.assertTrue(np.array_equal(error,results[:,0]),msg="error differs"); @@ -168,7 +166,6 @@ def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): self.assertTrue(np.array_equal(pos,results[:,2:5]),msg="pos differs"); self.assertTrue(np.array_equal(dir,results[:,5:8]),msg="dir differs"); self.assertTrue(np.array_equal(normal,results[:,8:11]),msg="normal differs"); - self.assertTrue(np.array_equal(opd,results[:,11]),msg="opd differs"); self.assertTrue(np.array_equal(intensity,results[:,12]),msg="intensity differs"); From 44179c76ddf4e085fa605b01fd52f6266250ae44 Mon Sep 17 00:00:00 2001 From: rhambach Date: Wed, 25 May 2016 23:49:16 +0200 Subject: [PATCH 12/23] add function zGetOpticalPathDifferenceArray() for calculating OPD - also ensure contigous and aligned arrays - example works --- pyzdde/arraytrace/arrayTraceClient.c | 83 ++++++++++++++- pyzdde/arraytrace/arrayTraceClient.h | 6 ++ pyzdde/arraytrace/numpy_interface.py | 144 ++++++++++++++++++++++++--- 3 files changed, 218 insertions(+), 15 deletions(-) diff --git a/pyzdde/arraytrace/arrayTraceClient.c b/pyzdde/arraytrace/arrayTraceClient.c index a354305..ea7a806 100644 --- a/pyzdde/arraytrace/arrayTraceClient.c +++ b/pyzdde/arraytrace/arrayTraceClient.c @@ -7,6 +7,9 @@ // Written by Kenneth Moore March 1999 // The original zclient.c and ArrayDemo.c files are also available in the same // directory for reference +// +// How to call these functions from Python ? see +// http://scipy.github.io/old-wiki/pages/Cookbook/Ctypes#NumPy.27s_ndpointer_with_ctypes_argtypes #include "arrayTraceClient.h" @@ -47,6 +50,11 @@ void rayTraceFunction(void) gPtr2RD = NULL; } +/* ---------------------------------------------------------------------------------- + ArrayTrace functions that accespt DDERAYDATA as argument + ---------------------------------------------------------------------------------- + */ + int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) { HWND hwnd; /* handle to client window */ @@ -94,14 +102,13 @@ int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) ---------------------------------------------------------------------------------- */ +// ---------------------------------------------------------------------------------- // Mode 0: similar to GetTrace (rays defined by field and pupil coordinates) int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], double intensity[], int wave_num[], int mode, int surf, int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], unsigned int timeout) { int i; - // how to call this function ? - // http://scipy.github.io/old-wiki/pages/Cookbook/Ctypes#NumPy.27s_ndpointer_with_ctypes_argtypes // allocate memory for list of structures expected by ZEMAX DDERAYDATA* RD = calloc(nrays+1,sizeof(*RD)); @@ -151,6 +158,78 @@ int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], } +// ---------------------------------------------------------------------------------- +// Calculate OPD for all rays indicated by normalized field and pupil coordinates +// as well as a list of wavenumbers. The return values are multidimensional arrays +// which can be indexed as opd[wave][field][pupil] +int __stdcall numpyOpticalPathDifference(int nField, double field[][2], + int nPupil, double pupil[][2], int nWave, int wave_num[], + int error[], int vigcode[], double opd[], double pos[][3], + double dir[][3], double intensity[], unsigned int timeout) +{ + int i,iField,iPupil,iWave; + int nrays = nWave*nField*nPupil; + + // allocate memory for list of structures expected by ZEMAX + DDERAYDATA* RD = calloc(nrays+1,sizeof(*RD)); + + // set parameters for raytrace (in 0'th element) + // see Zemax manual for meaning of the fields (do not infer from field names!) + RD[0].opd=0; // set type 0 (GetTrace) + RD[0].wave=0; // real ray trace, will be overwritten anyway by Zemax + RD[0].error=nrays; + RD[0].vigcode=0; + RD[0].want_opd=-1; // trace to image surface + + // initialize ray-structure with initial sampling + i=1; + for (iWave=0; iWave0 ! + opd[i] =RD[i+1].opd; + intensity[i]=RD[i+1].intensity; + error[i] =RD[i+1].error; + vigcode[i]=RD[i+1].vigcode; + } + } // end-if array trace suceeded + free(RD); + return RETVAL; +} + + +/* ---------------------------------------------------------------------------------- + DDE Communication (from Zemax examples) + ---------------------------------------------------------------------------------- + */ + LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { ATOM aApp, aTop, aItem; diff --git a/pyzdde/arraytrace/arrayTraceClient.h b/pyzdde/arraytrace/arrayTraceClient.h index 27e0773..397e003 100644 --- a/pyzdde/arraytrace/arrayTraceClient.h +++ b/pyzdde/arraytrace/arrayTraceClient.h @@ -36,6 +36,12 @@ DLL_EXPORT int __stdcall numpyGetTrace(int nrays, double field[][2], double pupi double intensity[], int wave_num[], int mode, int surf, int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], unsigned int timeout); +// wrapper for numpy arrays: calculate opd +DLL_EXPORT int __stdcall numpyOpticalPathDifference(int nField, double field[][2], + int nPupil, double pupil[][2], int nWave, int wave_num[], + int error[], int vigcode[], double opd[], double pos[][3], + double dir[][3], double intensity[], unsigned int timeout); + #ifdef __cplusplus } #endif diff --git a/pyzdde/arraytrace/numpy_interface.py b/pyzdde/arraytrace/numpy_interface.py index dce2f3f..18e71b0 100644 --- a/pyzdde/arraytrace/numpy_interface.py +++ b/pyzdde/arraytrace/numpy_interface.py @@ -54,13 +54,16 @@ def _is64bit(): _array_trace_lib = _ct.WinDLL(_dllpath + _dllName) # shorthands for CTypes +# Make sure the arrays are C-contigous, see +#http://stackoverflow.com/questions/26998223/what-is-the-difference-between-contiguous-and-non-contiguous-arrays +# Make sure the input array is aligned on proper boundaries for its data type. _INT = _ct.c_int; _INT1D = _np.ctypeslib.ndpointer(ndim=1,dtype=_np.int,flags=["C_CONTIGUOUS","ALIGNED"]) _INT2D = _np.ctypeslib.ndpointer(ndim=2,dtype=_np.int,flags=["C_CONTIGUOUS","ALIGNED"]) _DBL1D = _np.ctypeslib.ndpointer(ndim=1,dtype=_np.double,flags=["C_CONTIGUOUS","ALIGNED"]) _DBL2D = _np.ctypeslib.ndpointer(ndim=2,dtype=_np.double,flags=["C_CONTIGUOUS","ALIGNED"]) -def zGetTraceArray(field, pupil, intensity=None, waveNum=None, +def zGetTraceArray(field, pupil, intensity=1., waveNum=1, bParaxial=False, surf=-1, timeout=60000): """Trace large number of rays defined by their normalized field and pupil coordinates on lens file in the LDE of main Zemax application (not in the DDE server) @@ -69,7 +72,7 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, ---------- field : ndarray of shape (``numRays``,2) list of normalized field heights along x and y axis - px : ndarray of shape (``numRays``,2) + pupil : ndarray of shape (``numRays``,2) list of normalized heights in pupil coordinates, along x and y axis intensity : float or vector of length ``numRays``, optional initial intensities. If a vector of length ``numRays`` is given it is @@ -107,8 +110,7 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, intersection point of the ray with the requested surface intensity : list of reals the relative transmitted intensity of the ray, including any pupil - or surface apodization defined. Note mode 0 considers pupil apodization, - while mode 1 does not. + or surface apodization defined. If ray tracing fails, an RuntimeError is raised. @@ -124,26 +126,30 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, >>> pupil= np.transpose(grid[2:4]).reshape(-1,2); >>> # run array-trace >>> (error,vigcode,pos,dir,normal,intensity) = \\ - >>> zGetTraceArray(field,pupil); + zGetTraceArray(field,pupil); >>> # plot results >>> plt.scatter(pos[:,0],pos[:,1]) """ + # ensure correct memory alignment of input arrays + field = _np.require(field,dtype=_np.double,requirements=['C','A']); + pupil = _np.require(pupil,dtype=_np.double,requirements=['C','A']); + intensity=_np.require(intensity,dtype=_np.double,requirements=['C','A']); + waveNum =_np.require(waveNum,dtype=_np.int,requirements=['C','A']); + # handle input arguments nRays = field.shape[0]; assert (nRays,2) == field.shape == pupil.shape, 'field and pupil should have shape (nRays,2)' - if intensity is None: intensity=1; - if _np.isscalar(intensity): intensity=_np.full(nRays,intensity,dtype=_np.double); - if waveNum is None: waveNum=1; - if _np.isscalar(waveNum): waveNum=_np.full(nRays,waveNum,dtype=_np.int); + if intensity.size==1: intensity=_np.full(nRays,intensity,dtype=_np.double); + if waveNum.size==1: waveNum=_np.full(nRays,waveNum,dtype=_np.int); assert intensity.shape == (nRays,), 'intensity must be scalar or a vector of length nRays' assert waveNum.shape == (nRays,), 'waveNum must be scalar or a vector of length nRays' - # set up output arguments + # allocate memory for return values error=_np.zeros(nRays,dtype=_np.int); vigcode=_np.zeros(nRays,dtype=_np.int); - pos=_np.zeros((nRays,3)); - dir=_np.zeros((nRays,3)); - normal=_np.zeros((nRays,3)); + pos=_np.zeros((nRays,3),dtype=_np.double); + dir=_np.zeros((nRays,3),dtype=_np.double); + normal=_np.zeros((nRays,3),dtype=_np.double); # numpyGetTrace(int nrays, double field[][2], double pupil[][2], double intensity[], # int wave_num[], int mode, int surf, int error[], int vigcode[], double pos[][3], @@ -162,6 +168,96 @@ def zGetTraceArray(field, pupil, intensity=None, waveNum=None, return (error,vigcode,pos,dir,normal,intensity); +def zGetOpticalPathDifferenceArray(field,pupil,waveNum=1,timeout=60000): + """ + Calculates the optical path difference (OPD) between real ray and + real chief ray at the image plane for a large number of rays. The lens + file in the LDE of main Zemax application (not in the DDE server) is used. + + Parameters + ---------- + field : ndarray of shape (``nField``,2) + list of normalized field heights along x and y axis + pupil : ndarray of shape (``nPupil``,2) + list of normalized heights in pupil coordinates, along x and y axis + waveNum : integer or vector of length ``nWave``, optional + list of wavelength numbers. Default: single wavelength number equal to 1. + timeout : integer, optional + command timeout specified in milli-seconds (default: 1min), at least 1s + + Returns + ------- + error : ndarray of shape (nWave,nField,nPupil) + * =0: ray traced successfully + * <0: ray missed the surface number indicated by ``error`` + * >0: total internal reflection of ray at the surface number given by ``-error`` + vigcode : ndarray of shape (nWave,nField,nPupil) + indicates if ray was vignetted (vigcode==1) or not (vigcode=0) + opd : ndarray of shape (nWave,nField,nPupil) + computed optical path difference in waves, corresponding to the + wavelength of the individual ray + pos : ndarray of shape (nWave,nField,nPupil,3) + local image coordinates ``(x,y,z)`` of each ray + dir : ndarray of shape (nWave,nField,nPupil,3) + local direction cosines ``(l,m,n)`` at the image surface + intensity : ndarray of shape (nWave,nField,nPupil) + the relative transmitted intensity of the ray, including any pupil + or surface apodization defined. + + If ray tracing fails, an RuntimeError is raised. + + Examples + -------- + >>> import numpy as np + >>> import matplotlib.pylab as plt + >>> # pupil sampling along diagonal (px,px) + >>> NP=51; px = _np.linspace(-1,1,NP); + >>> pupil = _np.vstack((px,px)).T; + >>> (error,vigcode,opd,pos,dir,intensity) = \\ + zGetOpticalPathDifferenceArray(np.zeros((1,2)),pupil); + >>> plt.plot(px,opd[0,0,:]) + """ + # ensure correct memory alignment for input arrays + field = _np.require(field,dtype=_np.double,requirements=['C','A']); + pupil = _np.require(pupil,dtype=_np.double,requirements=['C','A']); + waveNum = _np.require(_np.atleast_1d(waveNum),dtype=_np.int,requirements=['C','A']); + + # handle input arguments + nField = field.shape[0]; assert field.shape == (nField,2), 'field must have shape (nField,2)' + nPupil = pupil.shape[0]; assert pupil.shape == (nPupil,2), 'field must have shape (nPupil,2)' + nWave = waveNum.shape[0]; + nRays = nWave*nField*nPupil; + + # allocate memory for return values + error=_np.zeros(nRays,dtype=_np.int); + vigcode=_np.zeros(nRays,dtype=_np.int); + opd=_np.zeros(nRays,dtype=_np.double); + pos=_np.zeros((nRays,3),dtype=_np.double); + dir=_np.zeros((nRays,3),dtype=_np.double); + intensity=_np.zeros(nRays,dtype=_np.double); + + # int numpyOpticalPathDifference(int nField, double field[][2], + # int nPupil, double pupil[][2], int nWave, int wave_num[], + # int error[], int vigcode[], double opd[], double pos[][3], + # double dir[][3], double intensity[], unsigned int timeout) + # result arrays are multidimensional arrays indexed as [wave][field][pupil] + _numpyOPD = _array_trace_lib.numpyOpticalPathDifference + _numpyOPD.restype = _INT + _numpyOPD.argtypes= [_INT,_DBL2D, _INT,_DBL2D, _INT,_INT1D, + _INT1D,_INT1D,_DBL1D,_DBL2D,_DBL2D,_DBL1D,_ct.c_uint] + ret = _numpyOPD(nField,field,nPupil,pupil,nWave,waveNum, + error,vigcode,opd,pos,dir,intensity,timeout) + # analyse error - flag + if ret==-1: raise RuntimeError("Couldn't retrieve data in PostArrayTraceMessage.") + if ret==-999: raise RuntimeError("Couldn't communicate with Zemax."); + if ret==-998: raise RuntimeError("Timeout reached after %dms"%timeout); + + # reshape arrays as multidimensional arrays + s1=(nWave,nField,nPupil); + s3=(nWave,nField,nPupil,3); + + return (error.reshape(s1),vigcode.reshape(s1),opd.reshape(s1), + pos.reshape(s3),dir.reshape(s3),intensity.reshape(s1)); # ########################################################################### @@ -194,9 +290,31 @@ def _test_zGetTraceArray(): print("Success!") +def _test_zOPDArray(): + """very basic test for the zGetTraceNumpy function + """ + # Basic test of the module functions + print("Basic test of zGetTraceNumpy module:") + NP=21; px = _np.linspace(-1,1,NP); py=0*px; + pupil = _np.vstack((px,py)).T; + NF=5; hx = _np.linspace(-1,1,NF); hy=0*hx; + field = _np.vstack((hx,hy)).T; + (error,vigcode,opd,pos,dir,intensity) = \ + zGetOpticalPathDifferenceArray(field,pupil); + + print(" number of rays: %d" % opd.size); + if opd.size<1e5: + import matplotlib.pylab as plt + plt.figure(); + for f in xrange(NF): + plt.plot(px,opd[0,f],label="hx=%5.3f"%field[f,0]); + plt.legend(loc=0); + print("Success!") if __name__ == '__main__': # run the test functions _test_zGetTraceArray() + _test_zOPDArray() + \ No newline at end of file From e78216c60fdb42969b8df32ac5125d8379e6f9fd Mon Sep 17 00:00:00 2001 From: rhambach Date: Thu, 26 May 2016 13:58:39 +0200 Subject: [PATCH 13/23] split field and pupil arguments into hx,hy,px,py for ArrayTrace functions - this simplifies construction of field and pupil coordinates considerably (e.g., meshgrid always generates x,y pairs, on-axis values like py=0 become trivial) --- pyzdde/arraytrace/arrayTraceClient.c | 22 ++--- pyzdde/arraytrace/arrayTraceClient.h | 6 +- pyzdde/arraytrace/numpy_interface.py | 124 +++++++++++++-------------- test/arrayTraceTest.py | 18 ++-- 4 files changed, 83 insertions(+), 87 deletions(-) diff --git a/pyzdde/arraytrace/arrayTraceClient.c b/pyzdde/arraytrace/arrayTraceClient.c index ea7a806..f94168f 100644 --- a/pyzdde/arraytrace/arrayTraceClient.c +++ b/pyzdde/arraytrace/arrayTraceClient.c @@ -104,7 +104,7 @@ int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) // ---------------------------------------------------------------------------------- // Mode 0: similar to GetTrace (rays defined by field and pupil coordinates) -int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], +int __stdcall numpyGetTrace(int nrays, double hx[], double hy[], double px[], double py[], double intensity[], int wave_num[], int mode, int surf, int error[], int vigcode[], double pos[][3], double dir[][3], double normal[][3], unsigned int timeout) { @@ -123,10 +123,10 @@ int __stdcall numpyGetTrace(int nrays, double field[][2], double pupil[][2], // initialize ray-structure with initial sampling for (i=0; i>> import matplotlib.pylab as plt >>> # cartesian sampling in field an pupil >>> x = np.linspace(-1,1,4) - >>> px= np.linspace(-1,1,3) - >>> grid = np.meshgrid(x,x,px,px); - >>> field= np.transpose(grid[0:2]).reshape(-1,2); - >>> pupil= np.transpose(grid[2:4]).reshape(-1,2); + >>> p = np.linspace(-1,1,3) + >>> hx,hy,px,py = np.meshgrid(x,x,p,p); >>> # run array-trace >>> (error,vigcode,pos,dir,normal,intensity) = \\ - zGetTraceArray(field,pupil); + zGetTraceArray(hx,hy,px,py); >>> # plot results - >>> plt.scatter(pos[:,0],pos[:,1]) + >>> plt.scatter(pos[:,0],pos[:,1],c=_np.sqrt(hx**2+hy**2)) """ - # ensure correct memory alignment of input arrays - field = _np.require(field,dtype=_np.double,requirements=['C','A']); - pupil = _np.require(pupil,dtype=_np.double,requirements=['C','A']); - intensity=_np.require(intensity,dtype=_np.double,requirements=['C','A']); - waveNum =_np.require(waveNum,dtype=_np.int,requirements=['C','A']); # handle input arguments - nRays = field.shape[0]; - assert (nRays,2) == field.shape == pupil.shape, 'field and pupil should have shape (nRays,2)' - if intensity.size==1: intensity=_np.full(nRays,intensity,dtype=_np.double); - if waveNum.size==1: waveNum=_np.full(nRays,waveNum,dtype=_np.int); - assert intensity.shape == (nRays,), 'intensity must be scalar or a vector of length nRays' - assert waveNum.shape == (nRays,), 'waveNum must be scalar or a vector of length nRays' + hx,hy,px,py = _np.atleast_1d(hx,hy,px,py); + nRays = max(hx.size,hy.size,px.size,py.size); + hx = _init_list1d(hx,nRays,_np.double,'hx'); + hy = _init_list1d(hy,nRays,_np.double,'hy'); + px = _init_list1d(px,nRays,_np.double,'px'); + py = _init_list1d(py,nRays,_np.double,'py'); + intensity = _init_list1d(intensity,nRays,_np.double,'intensity'); + waveNum = _init_list1d(waveNum,nRays,_np.int,'waveNum'); # allocate memory for return values error=_np.zeros(nRays,dtype=_np.int); @@ -151,15 +156,15 @@ def zGetTraceArray(field, pupil, intensity=1., waveNum=1, dir=_np.zeros((nRays,3),dtype=_np.double); normal=_np.zeros((nRays,3),dtype=_np.double); - # numpyGetTrace(int nrays, double field[][2], double pupil[][2], double intensity[], - # int wave_num[], int mode, int surf, int error[], int vigcode[], double pos[][3], - # double dir[][3], double normal[][3], unsigned int timeout); + # numpyGetTrace(int nrays, double hx[], double hy[], double px[], double py[], + # double intensity[], int wave_num[], int mode, int surf, int error[], + # int vigcode[], double pos[][3], double dir[][3], double normal[][3], unsigned int timeout); _numpyGetTrace = _array_trace_lib.numpyGetTrace _numpyGetTrace.restype = _INT - _numpyGetTrace.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT, - _INT1D,_INT1D,_DBL2D,_DBL2D,_DBL2D,_ct.c_uint] - ret = _numpyGetTrace(nRays,field,pupil,intensity,waveNum,int(bParaxial),surf, - error,vigcode,pos,dir,normal,timeout) + _numpyGetTrace.argtypes= [_INT,_DBL1D,_DBL1D,_DBL1D,_DBL1D,_DBL1D,_INT1D,_INT, + _INT,_INT1D,_INT1D,_DBL2D,_DBL2D,_DBL2D,_ct.c_uint] + ret = _numpyGetTrace(nRays,hx,hy,px,py,intensity,waveNum,int(bParaxial), + surf,error,vigcode,pos,dir,normal,timeout) # analyse error - flag if ret==-1: raise RuntimeError("Couldn't retrieve data in PostArrayTraceMessage.") if ret==-999: raise RuntimeError("Couldn't communicate with Zemax."); @@ -168,7 +173,7 @@ def zGetTraceArray(field, pupil, intensity=1., waveNum=1, return (error,vigcode,pos,dir,normal,intensity); -def zGetOpticalPathDifferenceArray(field,pupil,waveNum=1,timeout=60000): +def zGetOpticalPathDifferenceArray(hx,hy, px,py, waveNum=1,timeout=60000): """ Calculates the optical path difference (OPD) between real ray and real chief ray at the image plane for a large number of rays. The lens @@ -176,9 +181,9 @@ def zGetOpticalPathDifferenceArray(field,pupil,waveNum=1,timeout=60000): Parameters ---------- - field : ndarray of shape (``nField``,2) + hx,hy : float or vector of length ``nField`` list of normalized field heights along x and y axis - pupil : ndarray of shape (``nPupil``,2) + px,py : float or vector of length ``nPupil`` list of normalized heights in pupil coordinates, along x and y axis waveNum : integer or vector of length ``nWave``, optional list of wavelength numbers. Default: single wavelength number equal to 1. @@ -211,23 +216,23 @@ def zGetOpticalPathDifferenceArray(field,pupil,waveNum=1,timeout=60000): >>> import numpy as np >>> import matplotlib.pylab as plt >>> # pupil sampling along diagonal (px,px) - >>> NP=51; px = _np.linspace(-1,1,NP); - >>> pupil = _np.vstack((px,px)).T; + >>> NP=51; p = _np.linspace(-1,1,NP); >>> (error,vigcode,opd,pos,dir,intensity) = \\ - zGetOpticalPathDifferenceArray(np.zeros((1,2)),pupil); - >>> plt.plot(px,opd[0,0,:]) + zGetOpticalPathDifferenceArray(0,0,p,p); + >>> plt.plot(p,opd[0,0,:]) """ - # ensure correct memory alignment for input arrays - field = _np.require(field,dtype=_np.double,requirements=['C','A']); - pupil = _np.require(pupil,dtype=_np.double,requirements=['C','A']); - waveNum = _np.require(_np.atleast_1d(waveNum),dtype=_np.int,requirements=['C','A']); - + # handle input arguments - nField = field.shape[0]; assert field.shape == (nField,2), 'field must have shape (nField,2)' - nPupil = pupil.shape[0]; assert pupil.shape == (nPupil,2), 'field must have shape (nPupil,2)' - nWave = waveNum.shape[0]; - nRays = nWave*nField*nPupil; - + hx,hy,px,py,waveNum = _np.atleast_1d(hx,hy,px,py,waveNum); + nField = max(hx.size,hy.size) + nPupil = max(px.size,py.size); + nWave = waveNum.size; + nRays = nWave*nField*nPupil; + hx = _init_list1d(hx,nField,_np.double,'hx'); + hy = _init_list1d(hy,nField,_np.double,'hy'); + px = _init_list1d(px,nPupil,_np.double,'px'); + py = _init_list1d(py,nPupil,_np.double,'py'); + # allocate memory for return values error=_np.zeros(nRays,dtype=_np.int); vigcode=_np.zeros(nRays,dtype=_np.int); @@ -236,16 +241,16 @@ def zGetOpticalPathDifferenceArray(field,pupil,waveNum=1,timeout=60000): dir=_np.zeros((nRays,3),dtype=_np.double); intensity=_np.zeros(nRays,dtype=_np.double); - # int numpyOpticalPathDifference(int nField, double field[][2], - # int nPupil, double pupil[][2], int nWave, int wave_num[], + # int numpyOpticalPathDifference(int nField, double hx[], double hy[], + # int nPupil, double px[], double py[], int nWave, int wave_num[], # int error[], int vigcode[], double opd[], double pos[][3], # double dir[][3], double intensity[], unsigned int timeout) # result arrays are multidimensional arrays indexed as [wave][field][pupil] _numpyOPD = _array_trace_lib.numpyOpticalPathDifference _numpyOPD.restype = _INT - _numpyOPD.argtypes= [_INT,_DBL2D, _INT,_DBL2D, _INT,_INT1D, + _numpyOPD.argtypes= [_INT,_DBL1D,_DBL1D, _INT,_DBL1D,_DBL1D, _INT,_INT1D, _INT1D,_INT1D,_DBL1D,_DBL2D,_DBL2D,_DBL1D,_ct.c_uint] - ret = _numpyOPD(nField,field,nPupil,pupil,nWave,waveNum, + ret = _numpyOPD(nField,hx,hy,nPupil,px,py,nWave,waveNum, error,vigcode,opd,pos,dir,intensity,timeout) # analyse error - flag if ret==-1: raise RuntimeError("Couldn't retrieve data in PostArrayTraceMessage.") @@ -272,13 +277,10 @@ def _test_zGetTraceArray(): # Basic test of the module functions print("Basic test of zGetTraceNumpy module:") x = _np.linspace(-1,1,4) - px= _np.linspace(-1,1,3) - grid = _np.meshgrid(x,x,px,px); - field= _np.transpose(grid[0:2]).reshape(-1,2); - pupil= _np.transpose(grid[2:4]).reshape(-1,2); - + p = _np.linspace(-1,1,3) + hx,hy,px,py = _np.meshgrid(x,x,p,p); (error,vigcode,pos,dir,normal,intensity) = \ - zGetTraceArray(field,pupil,bParaxial=False,surf=-1); + zGetTraceArray(hx,hy,px,py,bParaxial=False,surf=-1); print(" number of rays: %d" % len(pos)); if len(pos)<1e5: @@ -286,7 +288,7 @@ def _test_zGetTraceArray(): from mpl_toolkits.mplot3d import Axes3D fig = plt.figure() ax = fig.add_subplot(111,projection='3d') - ax.scatter(*pos.T,c=_np.linalg.norm(pupil,axis=1)); + ax.scatter(*pos.T,c=_np.sqrt(px**2+py**2)); print("Success!") @@ -295,19 +297,17 @@ def _test_zOPDArray(): """ # Basic test of the module functions print("Basic test of zGetTraceNumpy module:") - NP=21; px = _np.linspace(-1,1,NP); py=0*px; - pupil = _np.vstack((px,py)).T; - NF=5; hx = _np.linspace(-1,1,NF); hy=0*hx; - field = _np.vstack((hx,hy)).T; + NP=21; px = _np.linspace(-1,1,NP); + NF=5; hx = _np.linspace(-1,1,NF); (error,vigcode,opd,pos,dir,intensity) = \ - zGetOpticalPathDifferenceArray(field,pupil); + zGetOpticalPathDifferenceArray(hx,0,px,0); print(" number of rays: %d" % opd.size); if opd.size<1e5: import matplotlib.pylab as plt plt.figure(); for f in xrange(NF): - plt.plot(px,opd[0,f],label="hx=%5.3f"%field[f,0]); + plt.plot(px,opd[0,f],label="hx=%5.3f"%hx[f]); plt.legend(loc=0); print("Success!") @@ -316,5 +316,3 @@ def _test_zOPDArray(): # run the test functions _test_zGetTraceArray() _test_zOPDArray() - - \ No newline at end of file diff --git a/test/arrayTraceTest.py b/test/arrayTraceTest.py index 622862c..c5199d1 100644 --- a/test/arrayTraceTest.py +++ b/test/arrayTraceTest.py @@ -108,15 +108,14 @@ def test_zGetTraceNumpy(self): self.ln.zPushLens(1); # set-up field and pupil sampling nr = 22; np.random.seed(0); - field = 2*np.random.rand(nr,2)-1; hx,hy = field.T; - pupil = 2*np.random.rand(nr,2)-1; px,py = pupil.T; + hx,hy,px,py = 2*np.random.rand(4,nr)-1; w = 1; # wavenum # run array trace (C-extension), returns (error,vigcode,pos,dir,normal,opd,intensity) mode_descr = ("real","paraxial") for mode in (0,1): print(" compare with GetTrace for %s raytrace"% mode_descr[mode]); - ret = nt.zGetTraceArray(field,pupil,bParaxial=(mode==1),waveNum=w,surf=-1); + ret = nt.zGetTraceArray(hx,hy,px,py,bParaxial=(mode==1),waveNum=w,surf=-1); ret = np.column_stack(ret).T; # compare with results from GetTrace, returns (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) @@ -141,13 +140,12 @@ def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): # set-up field and pupil sampling nr = 22; rd = at.getRayDataArray(nr) - pupil = 2*np.random.rand(nr,2)-1; - field = 2*np.random.rand(nr,2)-1; + hx,hy,px,py = 2*np.random.rand(4,nr)-1; for k in xrange(nr): - rd[k+1].x = field[k,0]; - rd[k+1].y = field[k,1]; - rd[k+1].z = pupil[k,0]; - rd[k+1].l = pupil[k,1]; + rd[k+1].x = hx[k]; + rd[k+1].y = hy[k]; + rd[k+1].z = px[k]; + rd[k+1].l = py[k]; rd[k+1].intensity = 1.0; rd[k+1].wave = 1; rd[k+1].want_opd = 0; @@ -158,7 +156,7 @@ def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): r.Exr,r.Eyr,r.Ezr,r.opd,r.intensity] for r in rd[1:]] ); # results of GetTraceArray (error,vigcode,pos,dir,normal,intensity) = \ - nt.zGetTraceArray(field,pupil,bParaxial=0); + nt.zGetTraceArray(hx,hy,px,py,bParaxial=0); # compare self.assertTrue(np.array_equal(error,results[:,0]),msg="error differs"); From 9b23012820a18171de3c9eeedbae75703de75a8d Mon Sep 17 00:00:00 2001 From: rhambach Date: Thu, 26 May 2016 15:35:07 +0200 Subject: [PATCH 14/23] add unit-test for zGetOpticalPathDifference() - introduce common function for comparing single raytrace and array raytrace (maybe not very easy to understand) --- test/arrayTraceTest.py | 85 +++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/test/arrayTraceTest.py b/test/arrayTraceTest.py index c5199d1..5bc1655 100644 --- a/test/arrayTraceTest.py +++ b/test/arrayTraceTest.py @@ -98,37 +98,71 @@ def test_zGetTraceArray(self): msg+= '%10s %12g %12g \n'%(ret_descr[j],ret[j,i],reference[j]); self.assertTrue(np.all(is_close), msg=msg); - - - def test_zGetTraceNumpy(self): - print("\nTEST: arraytrace.numpy_interface.zGetTraceArray()") + def compare_array_with_single_trace(self,strace,atrace,param_descr,ret_descr,nr=22,seed=0): + """ + helper function for comparing array raytrace and single raytrace functions + for nr random rays which are constructed from given params (e.g. ['hx','hy','px','py']). + The random number generator is initialized with given seed to ensure reproducibility. + """ # Load a lens file into the LDE filename = get_test_file() self.ln.zLoadFile(filename) self.ln.zPushLens(1); # set-up field and pupil sampling - nr = 22; np.random.seed(0); - hx,hy,px,py = 2*np.random.rand(4,nr)-1; - w = 1; # wavenum + np.random.seed(seed); + params = 2*np.random.rand(len(param_descr),nr)-1; + # perform array trace + aret = atrace(*params) + + # compare with results from single raytrace + for i in xrange(nr): + sret = strace(*params[:,i]); + is_close = np.isclose(aret[:,i], np.asarray(sret)); + msg = 'array and single raytrace differ for ray #%d:\n' % i; + msg+= ' initial ray parameters: (%s)\n' % ",".join(param_descr); + msg+= ' ' + str(params[:,i]) + "\n"; + msg+= ' parameter array-trace single-trace \n'; + for j in np.arange(aret.shape[0])[~is_close]: + msg+= '%10s %12g %12g \n'%(ret_descr[j],aret[j,i],sret[j]); + self.assertTrue(np.all(is_close), msg=msg); + + def test_zGetTraceNumpy(self): + print("\nTEST: arraytrace.numpy_interface.zGetTraceArray()") + w = 1; # wavenum - # run array trace (C-extension), returns (error,vigcode,pos,dir,normal,opd,intensity) - mode_descr = ("real","paraxial") - for mode in (0,1): - print(" compare with GetTrace for %s raytrace"% mode_descr[mode]); - ret = nt.zGetTraceArray(hx,hy,px,py,bParaxial=(mode==1),waveNum=w,surf=-1); - ret = np.column_stack(ret).T; - - # compare with results from GetTrace, returns (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) - ret_descr = ('error','vigcode','x','y','z','l','m','n','Exr','Eyr','Ezr','intensity') - for i in xrange(nr): - reference = self.ln.zGetTrace(w,mode,-1,hx[i],hy[i],px[i],py[i]); - is_close = np.isclose(ret[:,i], np.asarray(reference)); - msg = 'zGetTraceArray differs from GetTrace for %s ray #%d:\n' % (mode_descr[mode],i); - msg+= ' field: (%f,%f), pupil: (%f,%f) \n' % (hx[i],hy[i],px[i],py[i]); - msg+= ' parameter zGetTraceArray zGetTrace \n'; - for j in np.arange(12)[~is_close]: - msg+= '%10s %12g %12g \n'%(ret_descr[j],ret[j,i],reference[j]); - self.assertTrue(np.all(is_close), msg=msg); + for mode,descr in [(0,"real"),(1,"paraxial")]: + print(" compare with GetTrace for %s raytrace"% descr); + # single trace (GetTrace), returns (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) + ret_descr = ('error','vigcode','x','y','z','l','m','n','l2','m2','n2','intensity') + def strace(hx,hy,px,py): + return self.ln.zGetTrace(w,mode,-1,hx,hy,px,py); + # array trace (C-extension), returns (error,vigcode,pos(3),dir(3),normal(3)intensity) + def atrace(hx,hy,px,py): + ret = nt.zGetTraceArray(hx,hy,px,py,bParaxial=(mode==1),waveNum=w,surf=-1); + return np.column_stack(ret).T; + # perform comparison + self.compare_array_with_single_trace(strace,atrace,('hx','hy','px','py'),ret_descr); + + + def test_zGetOpticalPathDifference(self): + print("\nTEST: arraytrace.numpy_interface.zGetOpticalPathDifference()") + w = 1; # wavenum + px,py=1,0.5; # we fix the pupil values, as GetOpticalPathDifference + # traces rays to all pupil points for each field point + # single trace (GetTrace,OPDX), returns (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) + ret_descr = ('error','vigcode','opd','x','y','z','l','m','n','intensity') + def strace(hx,hy): + (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity)=self.ln.zGetTrace(w,0,-1,hx,hy,px,py); # real ray trace to image surface + opd=self.ln.zGetOpticalPathDifference(hx,hy,px,py,ref=0,wave=w); # calculate OPD, ref: chief ray + vig=1 if vig<>0 else 0; # vignetting flag is only 0 or 1 in ArrayTrace, not the surface number + return (error,vig,opd,x,y,z,l,m,n,intensity); + # array trace (C-extension), returns (error,vigcode,opd,pos,dir,intensity) + def atrace(hx,hy): + ret = nt.zGetOpticalPathDifferenceArray(hx,hy,px,py,waveNum=w); + ret = map(lambda a: a.reshape((ret[0].size,-1)), ret); # reshape arguments as (nRays,...) + return np.hstack(ret).T; + # perform comparison + self.compare_array_with_single_trace(strace,atrace,('hx','hy'),ret_descr); def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): @@ -141,6 +175,7 @@ def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): nr = 22; rd = at.getRayDataArray(nr) hx,hy,px,py = 2*np.random.rand(4,nr)-1; + for k in xrange(nr): rd[k+1].x = hx[k]; rd[k+1].y = hy[k]; From e6291aa0be6325d8e6908637a6f6779dc85402eb Mon Sep 17 00:00:00 2001 From: rhambach Date: Thu, 26 May 2016 17:59:56 +0200 Subject: [PATCH 15/23] add zGetTraceDirectArray() function --- pyzdde/arraytrace/arrayTraceClient.c | 57 ++++++++ pyzdde/arraytrace/arrayTraceClient.h | 6 +- pyzdde/arraytrace/numpy_interface.py | 190 +++++++++++++++++++++++++-- 3 files changed, 242 insertions(+), 11 deletions(-) diff --git a/pyzdde/arraytrace/arrayTraceClient.c b/pyzdde/arraytrace/arrayTraceClient.c index f94168f..d5a88bf 100644 --- a/pyzdde/arraytrace/arrayTraceClient.c +++ b/pyzdde/arraytrace/arrayTraceClient.c @@ -158,6 +158,63 @@ int __stdcall numpyGetTrace(int nrays, double hx[], double hy[], double px[], do } +// ---------------------------------------------------------------------------------- +// Mode 1: similar to GetTraceDirect (rays defined by position and direction cosines on any starting surface) +int __stdcall numpyGetTraceDirect(int nrays, double startpos[][3], double startdir[][3], + double intensity[], int wave_num[], int mode, int startsurf, int lastsurf, int error[], + int vigcode[], double pos[][3], double dir[][3], double normal[][3], unsigned int timeout) +{ + int i; + + // allocate memory for list of structures expected by ZEMAX + DDERAYDATA* RD = calloc(nrays+1,sizeof(*RD)); + + // set parameters for raytrace (in 0'th element) + // see Zemax manual for meaning of the fields (do not infer from field names!) + RD[0].opd=1; // set type 1 (GetTrace) + RD[0].wave=mode; // 0 for real rays, 1 for paraxial rays + RD[0].error=nrays; + RD[0].vigcode=startsurf; // the surface on which the coordinates start + RD[0].want_opd=lastsurf; // surface to trace to, -1 for image, or any valid surface number + + // initialize ray-structure with initial sampling + for (i=0; i0: total internal reflection of ray at the surface number given by ``-error`` + vigcode : list of integers + the first surface where the ray was vignetted. Unless an error occurs + at that surface or subsequent to that surface, the ray will continue + to trace to the requested surface. + pos : ndarray of shape (``numRays``,3) + local coordinates ``(x,y,z)`` of each ray on the requested last surface + dir : ndarray of shape (``numRays``,3) + local direction cosines ``(l,m,n)`` after refraction into the media + following the requested last surface. + normal : ndarray of shape (``numRays``,3) + local direction cosines ``(l2,m2,n2)`` of the surface normals at the + intersection point of the ray with the requested last surface + intensity : list of reals + the relative transmitted intensity of the ray, including any pupil + or surface apodization defined. + + If ray tracing fails, an RuntimeError is raised. + + Examples + -------- + >>> import numpy as np + >>> import matplotlib.pylab as plt + >>> import pyzdde.zdde as pyz + >>> ln = pyz.createLink() + >>> # launch rays from same from off-axis field point + >>> # we create initial pos and dir using zGetTraceArray + >>> nRays=7; + >>> startsurf= 1; # in case of collimated input beam + >>> lastsurf = ln.zGetNumSurf(); + >>> hx,hy,px,py = 0, 0.5, 0, np.linspace(-1,1,nRays); + >>> (_,_,pos,dir,_,_) = zGetTraceArray(hx,hy,px,py,bParaxial=False,surf=startsurf); + >>> # trace ray until last surface + >>> points = np.zeros((lastsurf+1,nRays,3)); # indexing: surf,ray,coord + >>> z0=0; points[startsurf]=pos; # ray intersection points on starting surface + >>> for isurf in xrange(startsurf,lastsurf): + >>> # trace to next surface + >>> (error,vigcode,pos,dir,_,_)=zGetTraceDirectArray(pos,dir,bParaxial=False,startSurf=isurf,lastSurf=isurf+1); + >>> points[isurf+1]=pos; + >>> points[isurf+1,vigcode<>0]=np.nan; # remove vignetted rays + >>> # add thickness of current surface (assumes absence of tilts or decenters in system) + >>> z0+=ln.zGetThickness(isurf); + >>> points[isurf+1,:,2]+=z0; + >>> print(" surface #%d at z-position z=%f" % (isurf+1,z0)); + >>> # plot rays in y-z section + >>> x,y,z = points[startsurf:].T; + >>> ax=plt.subplot(111,aspect='equal') + >>> ax.plot(z.T,y.T,'.-') + >>> ln.close(); + """ + # ensure correct memory alignment of input arrays + startpos = _np.require(startpos,dtype=_np.double,requirements=['C','A']); + startdir = _np.require(startdir,dtype=_np.double,requirements=['C','A']); + intensity=_np.require(intensity,dtype=_np.double,requirements=['C','A']); + waveNum =_np.require(waveNum,dtype=_np.int,requirements=['C','A']); + + # handle input arguments + nRays = startpos.shape[0]; + assert (nRays,3) == startpos.shape == startdir.shape, 'startpos and startdir should have shape (nRays,3)' + if intensity.size==1: intensity=_np.full(nRays,intensity,dtype=_np.double); + if waveNum.size==1: waveNum=_np.full(nRays,waveNum,dtype=_np.int); + assert intensity.shape == (nRays,), 'intensity must be scalar or a vector of length nRays' + assert waveNum.shape == (nRays,), 'waveNum must be scalar or a vector of length nRays' + + # allocate memory for return values + error=_np.zeros(nRays,dtype=_np.int); + vigcode=_np.zeros(nRays,dtype=_np.int); + pos=_np.zeros((nRays,3),dtype=_np.double); + dir=_np.zeros((nRays,3),dtype=_np.double); + normal=_np.zeros((nRays,3),dtype=_np.double); + + # numpyGetTraceDirect(int nrays, double startpos[][3], double startdir[][3], + # double intensity[], int wave_num[], int mode, int startsurf, int lastsurf, int error[], + # int vigcode[], double pos[][3], double dir[][3], double normal[][3], unsigned int timeout) + _numpyGetTraceDirect = _array_trace_lib.numpyGetTraceDirect + _numpyGetTraceDirect.restype = _INT + _numpyGetTraceDirect.argtypes= [_INT,_DBL2D,_DBL2D,_DBL1D,_INT1D,_INT,_INT,_INT, + _INT1D,_INT1D,_DBL2D,_DBL2D,_DBL2D,_ct.c_uint] + ret = _numpyGetTraceDirect(nRays,startpos,startdir,intensity,waveNum, + int(bParaxial),startSurf,lastSurf,error,vigcode,pos,dir,normal,timeout) + # analyse error - flag + if ret==-1: raise RuntimeError("Couldn't retrieve data in PostArrayTraceMessage.") + if ret==-999: raise RuntimeError("Couldn't communicate with Zemax."); + if ret==-998: raise RuntimeError("Timeout reached after %dms"%timeout); + + return (error,vigcode,pos,dir,normal,intensity); + + def zGetOpticalPathDifferenceArray(hx,hy, px,py, waveNum=1,timeout=60000): """ Calculates the optical path difference (OPD) between real ray and @@ -272,10 +396,9 @@ def zGetOpticalPathDifferenceArray(hx,hy, px,py, waveNum=1,timeout=60000): # ########################################################################### def _test_zGetTraceArray(): - """very basic test for the zGetTraceNumpy function - """ + "very basic test for the zGetTraceNumpy function" # Basic test of the module functions - print("Basic test of zGetTraceNumpy module:") + print("Basic test of zGetTraceArray:") x = _np.linspace(-1,1,4) p = _np.linspace(-1,1,3) hx,hy,px,py = _np.meshgrid(x,x,p,p); @@ -289,14 +412,29 @@ def _test_zGetTraceArray(): fig = plt.figure() ax = fig.add_subplot(111,projection='3d') ax.scatter(*pos.T,c=_np.sqrt(px**2+py**2)); - print("Success!") - + + +def _test_zGetTraceArrayDirect(): + "very basic test for the zGetTraceNumpy function" + # Basic test of the module functions + print("Basic test of zGetTraceArrayDirect:") + x = _np.linspace(-1,1,4) + p = _np.linspace(-1,1,3) + hx,hy,px,py = _np.meshgrid(x,x,p,p); + # compare with zGetTraceArray results + (_,_,startpos,startdir,_,_) = \ + zGetTraceArray(hx,hy,px,py,bParaxial=False,surf=0); + ref = zGetTraceArray(hx,hy,px,py,bParaxial=False,surf=-1); + # GetTraceArrayDirect + ret = zGetTraceDirectArray(startpos,startdir,bParaxial=False,startSurf=0,lastSurf=-1); + ret_descr = ('error','vigcode','pos','dir','normal','intensity') + for i in xrange(len(ref)): + assert _np.allclose(ref[i],ret[i]), "difference in %s"%ret_descr[i]; def _test_zOPDArray(): - """very basic test for the zGetTraceNumpy function - """ + "very basic test for the zGetTraceNumpy function" # Basic test of the module functions - print("Basic test of zGetTraceNumpy module:") + print("Basic test of zGetOpticalPathDifference:") NP=21; px = _np.linspace(-1,1,NP); NF=5; hx = _np.linspace(-1,1,NF); (error,vigcode,opd,pos,dir,intensity) = \ @@ -309,10 +447,42 @@ def _test_zOPDArray(): for f in xrange(NF): plt.plot(px,opd[0,f],label="hx=%5.3f"%hx[f]); plt.legend(loc=0); - print("Success!") + +def _plot_2D_layout_from_array_trace(): + "plot raypath through system using array trace" + import matplotlib.pylab as plt + import pyzdde.zdde as pyz + ln = pyz.createLink() + # launch rays from same from off-axis field point + # we create initial pos and dir using zGetTraceArray + nRays=7; + startsurf= 1; # in case of collimated input beam + lastsurf = ln.zGetNumSurf(); + hx,hy,px,py = 0, 0.5, 0, _np.linspace(-1,1,nRays); + (_,_,pos,dir,_,_) = zGetTraceArray(hx,hy,px,py,bParaxial=False,surf=startsurf); + # trace ray until last surface + points = _np.zeros((lastsurf+1,nRays,3)); # indexing: surf,ray,coord + z0=0; points[startsurf]=pos; # ray intersection points on starting surface + for isurf in xrange(startsurf,lastsurf): + # trace to next surface + (error,vigcode,pos,dir,_,_)=zGetTraceDirectArray(pos,dir,bParaxial=False,startSurf=isurf,lastSurf=isurf+1); + points[isurf+1]=pos; + points[isurf+1,vigcode<>0]=_np.nan; # remove vignetted rays + # add thickness of current surface (assumes absence of tilts or decenters in system) + z0+=ln.zGetThickness(isurf); + points[isurf+1,:,2]+=z0; + print(" surface #%d at z-position z=%f" % (isurf+1,z0)); + # plot rays in y-z section + plt.figure(); + x,y,z = points[startsurf:].T; + ax=plt.subplot(111,aspect='equal') + ax.plot(z.T,y.T,'.-') + ln.close(); if __name__ == '__main__': # run the test functions _test_zGetTraceArray() + _test_zGetTraceArrayDirect() _test_zOPDArray() + _plot_2D_layout_from_array_trace() \ No newline at end of file From 83c5614829e62c9adf05f4de00fcf1d4b26ef690 Mon Sep 17 00:00:00 2001 From: rhambach Date: Fri, 27 May 2016 11:40:22 +0200 Subject: [PATCH 16/23] add unit-test for zGetTraceDirectArray() and update old tests --- test/arrayTraceTest.py | 188 ++++++++++++++++++++++++++++------------- 1 file changed, 131 insertions(+), 57 deletions(-) diff --git a/test/arrayTraceTest.py b/test/arrayTraceTest.py index 5bc1655..c750db9 100644 --- a/test/arrayTraceTest.py +++ b/test/arrayTraceTest.py @@ -28,6 +28,8 @@ class TestArrayTrace(unittest.TestCase): def setUpClass(self): print('RUNNING TESTS FOR MODULE \'%s\'.'% at.__file__) self.ln = pyz.createLink(); + if self.ln is None: + raise RuntimeError("Zemax DDE link could not be established. Please open Zemax."); self.ln.zGetUpdate(); @classmethod @@ -66,11 +68,43 @@ def test_getRayDataArray(self): self.assertEqual(rd[0].z,0.0) self.assertEqual(rd[0].error,1) - def test_zGetTraceArray(self): + def compare_array_with_single_trace(self,strace,atrace,param_descr,ret_descr,nr=22,seed=0): + """ + helper function for comparing array raytrace and single raytrace functions + for nr random rays which are constructed from given params (e.g. ['hx','hy','px','py']). + The random number generator is initialized with given seed to ensure reproducibility. + """ + # Load a lens file into the LDE + filename = get_test_file() + ret = self.ln.zLoadFile(filename); + if ret<>0: raise IOError("Could not load Zemax file '%s'. Error code %d" % (filename,ret)); + if not self.ln.zPushLensPermission(): + raise RuntimeError("Extensions not allowed to push lenses. Please enable in Zemax.") + self.ln.zPushLens(1); + # set-up field and pupil sampling + np.random.seed(seed); + params = 2*np.random.rand(len(param_descr),nr)-1; + # perform array trace + aret = atrace(*params) + + # compare with results from single raytrace + for i in xrange(nr): + sret = strace(*params[:,i]); + is_close = np.isclose(aret[:,i], np.asarray(sret)); + msg = 'array and single raytrace differ for ray #%d:\n' % i; + msg+= ' initial ray parameters: (%s)\n' % ",".join(param_descr); + msg+= ' ' + str(params[:,i]) + "\n"; + msg+= ' parameter array-trace single-trace \n'; + for j in np.arange(aret.shape[0])[~is_close]: + msg+= '%10s %12g %12g \n'%(ret_descr[j],aret[j,i],sret[j]); + self.assertTrue(np.all(is_close), msg=msg); + + + def test_zGetTraceArrayOLd(self): print("\nTEST: arraytrace.zGetTraceArray()") # Load a lens file into the Lde filename = get_test_file() - self.ln.zLoadFile(filename) + self.ln.zLoadFile(filename) self.ln.zPushLens(1); # set up field and pupil sampling with random values nr = 22; np.random.seed(0); @@ -98,33 +132,62 @@ def test_zGetTraceArray(self): msg+= '%10s %12g %12g \n'%(ret_descr[j],ret[j,i],reference[j]); self.assertTrue(np.all(is_close), msg=msg); - def compare_array_with_single_trace(self,strace,atrace,param_descr,ret_descr,nr=22,seed=0): - """ - helper function for comparing array raytrace and single raytrace functions - for nr random rays which are constructed from given params (e.g. ['hx','hy','px','py']). - The random number generator is initialized with given seed to ensure reproducibility. - """ + def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): + print("\nTEST: comparison zGetTraceArray from numpy_interface and raystruct_interface.") + w = 1; # wavenum + nr= 3; # number of rays + + for mode,descr in [(0,"real"),(1,"paraxial")]: + print(" compare zGetTraceNumpy (called with single ray) with zGetTraceArray() for %s raytrace"% descr); + # single trace (GetTrace), returns (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) + ret_descr = ('error','vigcode','x','y','z','l','m','n','l2','m2','n2','intensity') + def strace(hx,hy,px,py): + ret = nt.zGetTraceArray(hx,hy,px,py,intensity=1,waveNum=w,bParaxial=(mode==1),surf=-1) + return np.column_stack(ret).flatten(); + # array trace (C-extension), returns (error,vigcode,pos(3),dir(3),normal(3)intensity) + def atrace(hx,hy,px,py): + ret = at.zGetTraceArray(nr,list(hx),list(hy),list(px),list(py), + intensity=1,waveNum=w,mode=mode,surf=-1,want_opd=0); + mask = np.ones(13,dtype=np.bool); mask[-2]=False; # mask array for removing opd from ret + return np.column_stack(ret).T[mask]; + # perform comparison + self.compare_array_with_single_trace(strace,atrace,('hx','hy','px','py'),ret_descr,nr=nr); + + def test_cross_check_zArrayTrace_vs_zGetTraceNumpy_OLD(self): + print("\nTEST: comparison of zArrayTrace and zGetTraceNumpy OLD") # Load a lens file into the LDE filename = get_test_file() self.ln.zLoadFile(filename) self.ln.zPushLens(1); # set-up field and pupil sampling - np.random.seed(seed); - params = 2*np.random.rand(len(param_descr),nr)-1; - # perform array trace - aret = atrace(*params) + nr = 22; + rd = at.getRayDataArray(nr) + hx,hy,px,py = 2*np.random.rand(4,nr)-1; + + for k in xrange(nr): + rd[k+1].x = hx[k]; + rd[k+1].y = hy[k]; + rd[k+1].z = px[k]; + rd[k+1].l = py[k]; + rd[k+1].intensity = 1.0; + rd[k+1].wave = 1; + rd[k+1].want_opd = 0; + # results of zArrayTrace + ret = at.zArrayTrace(rd); + self.assertEqual(ret,0); + results = np.asarray( [[r.error,r.vigcode,r.x,r.y,r.z,r.l,r.m,r.n,\ + r.Exr,r.Eyr,r.Ezr,r.opd,r.intensity] for r in rd[1:]] ); + # results of GetTraceArray + (error,vigcode,pos,dir,normal,intensity) = \ + nt.zGetTraceArray(hx,hy,px,py,bParaxial=0); - # compare with results from single raytrace - for i in xrange(nr): - sret = strace(*params[:,i]); - is_close = np.isclose(aret[:,i], np.asarray(sret)); - msg = 'array and single raytrace differ for ray #%d:\n' % i; - msg+= ' initial ray parameters: (%s)\n' % ",".join(param_descr); - msg+= ' ' + str(params[:,i]) + "\n"; - msg+= ' parameter array-trace single-trace \n'; - for j in np.arange(aret.shape[0])[~is_close]: - msg+= '%10s %12g %12g \n'%(ret_descr[j],aret[j,i],sret[j]); - self.assertTrue(np.all(is_close), msg=msg); + # compare + self.assertTrue(np.array_equal(error,results[:,0]),msg="error differs"); + self.assertTrue(np.array_equal(vigcode,results[:,1]),msg="vigcode differs"); + self.assertTrue(np.array_equal(pos,results[:,2:5]),msg="pos differs"); + self.assertTrue(np.array_equal(dir,results[:,5:8]),msg="dir differs"); + self.assertTrue(np.array_equal(normal,results[:,8:11]),msg="normal differs"); + self.assertTrue(np.array_equal(intensity,results[:,12]),msg="intensity differs"); def test_zGetTraceNumpy(self): print("\nTEST: arraytrace.numpy_interface.zGetTraceArray()") @@ -164,42 +227,53 @@ def atrace(hx,hy): # perform comparison self.compare_array_with_single_trace(strace,atrace,('hx','hy'),ret_descr); - - def test_cross_check_zArrayTrace_vs_zGetTraceNumpy(self): - print("\nTEST: comparison of zArrayTrace and zGetTraceNumpy") - # Load a lens file into the LDE - filename = get_test_file() - self.ln.zLoadFile(filename) - self.ln.zPushLens(1); - # set-up field and pupil sampling - nr = 22; - rd = at.getRayDataArray(nr) - hx,hy,px,py = 2*np.random.rand(4,nr)-1; + def test_zGetTraceDirectNumpy(self): + print("\nTEST: arraytrace.numpy_interface.zGetTraceDirectArray()") + w = 1; # wavenum + startSurf=0 + lastSurf=-1 - for k in xrange(nr): - rd[k+1].x = hx[k]; - rd[k+1].y = hy[k]; - rd[k+1].z = px[k]; - rd[k+1].l = py[k]; - rd[k+1].intensity = 1.0; - rd[k+1].wave = 1; - rd[k+1].want_opd = 0; - # results of zArrayTrace - ret = at.zArrayTrace(rd); - self.assertEqual(ret,0); - results = np.asarray( [[r.error,r.vigcode,r.x,r.y,r.z,r.l,r.m,r.n,\ - r.Exr,r.Eyr,r.Ezr,r.opd,r.intensity] for r in rd[1:]] ); - # results of GetTraceArray - (error,vigcode,pos,dir,normal,intensity) = \ - nt.zGetTraceArray(hx,hy,px,py,bParaxial=0); + for mode,descr in [(0,"real"),(1,"paraxial")]: + print(" compare with GetTraceDirect for %s raytrace"% descr); + # single trace (GetTraceDirect), returns (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) + ret_descr = ('error','vigcode','x','y','z','l','m','n','l2','m2','n2','intensity') + def strace(x,y,z,l,m): + n = np.sqrt(1-0.5*l**2-0.5*m**2); # calculate z-direction (scale l,m to < 1/sqrt(2)) + return self.ln.zGetTraceDirect(w,mode,startSurf,lastSurf,x,y,z,l,m,n); + # array trace (C-extension), returns (error,vigcode,pos(3),dir(3),normal(3)intensity) + def atrace(x,y,z,l,m): + n = np.sqrt(1-0.5*l**2-0.5*m**2); # calculate z-direction + pos = np.stack((x,y,z),axis=1); + dir = np.stack((l,m,n),axis=1); + ret = nt.zGetTraceDirectArray(pos,dir,bParaxial=mode,startSurf=startSurf,lastSurf=lastSurf); + return np.column_stack(ret).T; + # perform comparison + self.compare_array_with_single_trace(strace,atrace,('x','y','z','l','m'),ret_descr); - # compare - self.assertTrue(np.array_equal(error,results[:,0]),msg="error differs"); - self.assertTrue(np.array_equal(vigcode,results[:,1]),msg="vigcode differs"); - self.assertTrue(np.array_equal(pos,results[:,2:5]),msg="pos differs"); - self.assertTrue(np.array_equal(dir,results[:,5:8]),msg="dir differs"); - self.assertTrue(np.array_equal(normal,results[:,8:11]),msg="normal differs"); - self.assertTrue(np.array_equal(intensity,results[:,12]),msg="intensity differs"); + # ----------------------------------------------------------------------------- + # test fails for real raytrace, as arrayTrace from Zemax does not return + # surface normal correctly. Should be handled in the python interface + # to avoid confusion + # ----------------------------------------------------------------------------- + def test_zGetTraceArrayOPD(self): + print("\nTEST: OPD for arraytrace.raystruct_interface.zGetTraceArray()") + w = 1; # wavenum + nr=30; # number of rays + mode=0;# only real raytrace works with want_opd + + # single trace (GetTrace), returns (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) + ret_descr = ('error','vigcode','x','y','z','l','m','n','l2','m2','n2','opd','intensity') + def strace(hx,hy,px,py): + opd=self.ln.zGetOpticalPathDifference(hx,hy,px,py,ref=0,wave=w); + (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) = self.ln.zGetTrace(w,mode,-1,hx,hy,px,py); + return (error,vig,x,y,z,l,m,n,l2,m2,n2,opd,intensity); + # array trace (C-extension), returns (error,vigcode,pos(3),dir(3),normal(3)intensity) + def atrace(hx,hy,px,py): + ret = at.zGetTraceArray(nr,list(hx),list(hy),list(px),list(py), + intensity=1,waveNum=w,mode=mode,surf=-1,want_opd=-1); + return np.column_stack(ret).T; + # perform comparison + self.compare_array_with_single_trace(strace,atrace,('hx','hy','px','py'),ret_descr,nr=nr); From f6507cc15df8c5d5e052c3d469e654dfcbfd7528 Mon Sep 17 00:00:00 2001 From: rhambach Date: Fri, 27 May 2016 15:33:52 +0200 Subject: [PATCH 17/23] create directories in makefile, correct comment --- pyzdde/arraytrace/makefile.win | 10 +++++++--- pyzdde/arraytrace/numpy_interface.py | 11 ----------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/pyzdde/arraytrace/makefile.win b/pyzdde/arraytrace/makefile.win index 73eebf8..a474d0e 100644 --- a/pyzdde/arraytrace/makefile.win +++ b/pyzdde/arraytrace/makefile.win @@ -19,7 +19,7 @@ NMAKE= nmake /nologo /C /f Makefile.win CGFLAGS=/c /MT /D_WINDOWS /D_USRDLL /D_WINDLL /EHsc /W4 /Zp1 CRFLAGS=/DNDEBUG /Ox -CDFLAGS=/Ge /MLd /Od /Zi +CDFLAGS=/Od /Zi #------------------------------------------------------------------------- # Linker-Flags @@ -77,7 +77,7 @@ all: debug32 debug64 release32 release64 @echo. @echo Done. -release: clean $(RELEASEDIR)\ArrayTrace.dll +release: clean $(RELEASEDIR) $(RELEASEDIR)\ArrayTrace.dll release32: @$(NMAKE) ARCH="Win32 Release" release release64: @@ -94,7 +94,7 @@ clean-all: @$(NMAKE) ARCH="x64 Debug" clean clean: - @-erase *.obj *.pdb *.ilk *.pch $(RELEASEDIR)\*.dll $(RELEASEDIR)\*.lib $(RELEASEDIR)\*.exp + @erase *.obj *.pdb *.ilk *.pch $(RELEASEDIR)\*.dll $(RELEASEDIR)\*.lib $(RELEASEDIR)\*.exp #------------------------------------------------------------------------- @@ -103,6 +103,10 @@ clean: arrayTraceClient.obj: arrayTraceClient.c arrayTraceClient.h $(CC) $(CFLAGS) arrayTraceClient.c + +$(RELEASEDIR): + @mkdir "$(RELEASEDIR)" + $(RELEASEDIR)\ArrayTrace.dll: arrayTraceClient.obj $(LINK) $(LFLAGS) /OUT:$@ $** diff --git a/pyzdde/arraytrace/numpy_interface.py b/pyzdde/arraytrace/numpy_interface.py index 7f3a052..1cbd147 100644 --- a/pyzdde/arraytrace/numpy_interface.py +++ b/pyzdde/arraytrace/numpy_interface.py @@ -20,17 +20,6 @@ Note ----- - -The parameter want_opd is very confusing as it alters the behavior of GetTraceArray. -If the calculation of OPD is requested for a ray, -- vigcode becomes a different meaning (seems to be 1 if vignetted, but no longer related to surface) -- bParaxial (mode) becomes inactive, Zemax always performs a real-raytrace ! -- the surface normal is not calculated -- if the calculation of the chief ray data is not requested for the first ray, - e.g. by setting all want_opd to 1, wrong OPD values are returned (without any relation to the real values) -- this affects only rays with want_opd<>0 -> i.e. if it is mixed, one obtains a total mess - - The pupil apodization seems to be always considered independent of the mode / bParaxial value in contrast to the note in the Zemax Manual (tested with Zemax 13 Release 2 SP 5 Premium 64bit) """ From e8a9eac509c8b599e902b06650026cbbf4787414 Mon Sep 17 00:00:00 2001 From: rhambach Date: Fri, 27 May 2016 15:42:05 +0200 Subject: [PATCH 18/23] correct typos and wrong datatype in ctype call of zArrayTrace --- pyzdde/arraytrace/arrayTraceClient.c | 4 ++-- pyzdde/arraytrace/raystruct_interface.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyzdde/arraytrace/arrayTraceClient.c b/pyzdde/arraytrace/arrayTraceClient.c index d5a88bf..91719a5 100644 --- a/pyzdde/arraytrace/arrayTraceClient.c +++ b/pyzdde/arraytrace/arrayTraceClient.c @@ -51,7 +51,7 @@ void rayTraceFunction(void) } /* ---------------------------------------------------------------------------------- - ArrayTrace functions that accespt DDERAYDATA as argument + ArrayTrace functions that accept DDERAYDATA as argument ---------------------------------------------------------------------------------- */ @@ -97,7 +97,7 @@ int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) } /* ---------------------------------------------------------------------------------- - ArrayTrace functions that accespt Numpy arrays as arguments + ArrayTrace functions that accept Numpy arrays as arguments avoids large overhead times in Python wrapper functions ---------------------------------------------------------------------------------- */ diff --git a/pyzdde/arraytrace/raystruct_interface.py b/pyzdde/arraytrace/raystruct_interface.py index 79ce87c..ed76244 100644 --- a/pyzdde/arraytrace/raystruct_interface.py +++ b/pyzdde/arraytrace/raystruct_interface.py @@ -75,7 +75,7 @@ def _is64bit(): # int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) _arrayTrace = _array_trace_lib.arrayTrace _arrayTrace.restype = _ct.c_int -_arrayTrace.argtypes = [_ct.POINTER(DdeArrayData), _ct.c_int] +_arrayTrace.argtypes = [_ct.POINTER(DdeArrayData), _ct.c_uint] def zArrayTrace(rd, timeout=5000): From 658a3eacb79fc1eeae68a58309aa54e410437a01 Mon Sep 17 00:00:00 2001 From: Ralf Hambach Date: Tue, 11 Jul 2017 17:37:09 +0200 Subject: [PATCH 19/23] add _context.py file to make tests executeable without installing package - using hopefully more portable version of determining current path, see https://stackoverflow.com/questions/714063/importing-modules-from-parent-folder/33532002#33532002 - handle exception correctly --- Test/checkDataItemCompleteness.py | 6 ++--- Test/pyZDDEscenariotest.py | 18 ++++---------- Test/pyZDDEunittest.py | 40 +++++++++++-------------------- Test/zfileutilsTest.py | 3 +-- pyzdde/arraytrace/__init__.py | 2 +- test/_context.py | 15 ++++++++++++ 6 files changed, 38 insertions(+), 46 deletions(-) create mode 100644 test/_context.py diff --git a/Test/checkDataItemCompleteness.py b/Test/checkDataItemCompleteness.py index a099f05..677bc80 100644 --- a/Test/checkDataItemCompleteness.py +++ b/Test/checkDataItemCompleteness.py @@ -13,9 +13,9 @@ from __future__ import print_function import os import inspect -import pyzdde.zdde as pyz -testdirectory = os.path.dirname(os.path.realpath(__file__)) +from _context import pyzdde, testdir +import pyzdde.zdde as pyz def main(): # Get data items (class methods) from PyZDDE @@ -34,7 +34,7 @@ def main(): # Get data items from textfile dataItemSet_zemax = [] - dataItemFile = open(testdirectory+os.path.sep+"zemaxDataItems.txt","r") + dataItemFile = open(os.path.join(testdir,"zemaxDataItems.txt"),"r") for line in dataItemFile: if line.rstrip() is not '': if not line.rstrip().startswith('#'): diff --git a/Test/pyZDDEscenariotest.py b/Test/pyZDDEscenariotest.py index 2e027a4..d82ccc4 100644 --- a/Test/pyZDDEscenariotest.py +++ b/Test/pyZDDEscenariotest.py @@ -9,24 +9,13 @@ #------------------------------------------------------------------------------- from __future__ import print_function import os -import sys import time -# Put both the "Test" and the "PyZDDE" directory in the python search path. -testdirectory = os.path.dirname(os.path.realpath(__file__)) -ind = testdirectory.find('Test') -pyzddedirectory = testdirectory[0:ind-1] -if testdirectory not in sys.path: - sys.path.append(testdirectory) -if pyzddedirectory not in sys.path: - sys.path.append(pyzddedirectory) - -# Import the pyzdde module -#import pyzdde +from _context import pyzdde, moduledir import pyzdde.zdde as pyz # ZEMAX file directory -zmxfp = pyzddedirectory+'\\ZMXFILES\\' +zmxfp = os.path.join(moduledir,'ZMXFILES'); def testSetup(): # Setup up the basic environment for the scenerio test @@ -65,7 +54,8 @@ def test_scenario_multipleChannel(): del ln2 # We can delete this object like this since no DDE conversation object was created for it. # Load a lens into the second ZEMAX DDE server - filename = zmxfp+"Cooke 40 degree field.zmx" + filename = os.path.join(zmxfp,'Cooke_40_degree_field.zmx'); + assert os.path.exists(filename), "file not found: '%s'" % filename ret = ln1.zLoadFile(filename) assert ret == 0 print("\nzLoadFile test successful") diff --git a/Test/pyZDDEunittest.py b/Test/pyZDDEunittest.py index 50b851c..839480f 100644 --- a/Test/pyZDDEunittest.py +++ b/Test/pyZDDEunittest.py @@ -10,20 +10,10 @@ from __future__ import division from __future__ import print_function import os -import sys import imp import unittest -# Put both the "Test" and the "PyZDDE" directory in the python search path. -testdirectory = os.path.dirname(os.path.realpath(__file__)) -#ind = testdirectory.find('Test') -pyzddedirectory = os.path.split(testdirectory)[0] - -if testdirectory not in sys.path: - sys.path.append(testdirectory) -if pyzddedirectory not in sys.path: - sys.path.append(pyzddedirectory) - +from _context import pyzdde, testdir, moduledir import pyzdde.zdde as pyzdde import pyzdde.zfileutils as zfile @@ -423,7 +413,6 @@ def test_zSetPOPSettings(self): paramN=srcParam, tPow=1, sampx=4, sampy=4, widex=40, widey=40, fibComp=1, fibType=0, fparamN=fibParam) - exception = None try: self.assertTrue(checkFileExist(sfilename), "Expected function to create settings file") @@ -453,14 +442,10 @@ def test_zSetPOPSettings(self): self.assertEqual(popinfo.blank, None, 'Expected None for blank phase field') self.assertEqual(popinfo.fibEffSys, None, 'Expected None for no fiber integral') self.assertEqual(popinfo.gridX, 128, 'Expected grid x be 128') - except Exception as exception: - pass # nothing to do here, raise it after cleaning up finally: # It is important to delete these settings files after the test. If not # deleted, they WILL interfere with the ohter POP tests deleteFile(sfilename) - if exception: - raise exception if TestPyZDDEFunctions.pRetVar: print('zSetPOPSettings test successful') @@ -477,7 +462,6 @@ def test_zModifyPOPSettings(self): paramN=srcParam, tPow=1, sampx=4, sampy=4, widex=40, widey=40, fibComp=1, fibType=0, fparamN=fibParam) - exception = None try: # Get POP info with the above settings popinfo = self.ln.zGetPOP(sfilename) @@ -493,14 +477,11 @@ def test_zModifyPOPSettings(self): print(popinfo) self.assertEqual(popinfo.totPow, 2.0, 'Expected tot pow 2.0') self.assertEqual(popinfo.gridX, 64, 'Expected grid x be 64') - except Exception as exception: - pass # nothing to do here, raise it after cleaning up finally: # It is important to delete these settings files after the test. If not # deleted, they WILL interfere with the ohter POP tests deleteFile(sfilename) - if exception: - raise exception + if TestPyZDDEFunctions.pRetVar: print('zModifyPOPSettings test successful') @@ -865,13 +846,13 @@ def test_zGetTextFile(self): ret = self.ln.zGetTextFile(preFileName,'Pre',"None",0) self.assertEqual(ret,-1) # filename path is absolute, however, doesn't have extension - textFileName = testdirectory + '\\' + os.path.splitext(preFileName)[0] + textFileName = os.path.join(testdir, os.path.splitext(preFileName)[0]); ret = self.ln.zGetTextFile(textFileName,'Pre',"None",0) self.assertEqual(ret,-1) # Request to dump prescription file, without providing a valid settings file # and flag = 0 ... so that the default settings will be used for the text # Create filename with full path - textFileName = testdirectory + '\\' + preFileName + textFileName = os.path.join(testdir, preFileName) ret = self.ln.zGetTextFile(textFileName,'Pre',"None",0) self.assertIn(ret,(0,-1,-998)) #ensure that the ret is any valid return if ret == -1: @@ -884,7 +865,7 @@ def test_zGetTextFile(self): ret = self.ln.zGetRefresh() settingsFileName = "Cooke_40_degree_field_PreSettings_OnlyCardinals.CFG" preFileName = 'Prescription_unitTest_01.txt' - textFileName = testdirectory + '\\' + preFileName + textFileName = os.path.join(testdir, preFileName) ret = self.ln.zGetTextFile(textFileName,'Pre',settingsFileName,1) self.assertIn(ret,(0,-1,-998)) #ensure that the ret is any valid return if ret == -1: @@ -1144,6 +1125,7 @@ def test_zModifySettings(self): ret = self.ln.zModifySettings('invalidFileName.CFG','LAY_RAYS', 5) self.assertEqual(ret, -1) # Pass valid parameters and string type value + # might fail if wrong Zemax version is used. ret = self.ln.zModifySettings(sfilename,'UN1_OPERAND', 'ZERN') self.assertEqual(ret, 0) if TestPyZDDEFunctions.pRetVar: @@ -1946,7 +1928,7 @@ def get_test_file(fileType='seq', settings=False, **kwargs): file : string/ tuple filenames are complete complete paths """ - zmxfp = os.path.join(pyzddedirectory, 'ZMXFILES') + zmxfp = os.path.join(moduledir, 'ZMXFILES') lensFile = ["Cooke_40_degree_field.zmx", "Double_Gauss_5_degree_field.ZMX", "LENS.ZMX",] @@ -2018,4 +2000,10 @@ def loadDefaultZMXfile2LDE(ln): ln.zPushLens(1) if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main(module='pyZDDEunittest'); + ## only run single test function, see https://docs.python.org/2/library/unittest.html + #test_function='test_zModifySettings'; + #suite = unittest.TestSuite(); + #suite.addTest(TestPyZDDEFunctions(test_function)) + #unittest.TextTestRunner().run(suite) + \ No newline at end of file diff --git a/Test/zfileutilsTest.py b/Test/zfileutilsTest.py index b498adf..bd536e6 100644 --- a/Test/zfileutilsTest.py +++ b/Test/zfileutilsTest.py @@ -13,8 +13,7 @@ #from struct import unpack, pack import pyzdde.zfileutils as zfu #import ctypes as _ctypes - -testdir = os.path.dirname(os.path.realpath(__file__)) +from _context import testdir #%% Helper functions diff --git a/pyzdde/arraytrace/__init__.py b/pyzdde/arraytrace/__init__.py index fed76ef..dd5bc80 100644 --- a/pyzdde/arraytrace/__init__.py +++ b/pyzdde/arraytrace/__init__.py @@ -1,3 +1,3 @@ __all__ = ['']; # for backward compatibility -from raystruct_interface import * \ No newline at end of file +from pyzdde.arraytrace.raystruct_interface import * \ No newline at end of file diff --git a/test/_context.py b/test/_context.py new file mode 100644 index 0000000..c5974e4 --- /dev/null +++ b/test/_context.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# AIM: run tests always on local version independent of installed packages +# see http://docs.python-guide.org/en/latest/writing/structure/ +# see https://stackoverflow.com/questions/714063/importing-modules-from-parent-folder/33532002#33532002 +from inspect import getsourcefile +import os.path +import sys + +current_path = os.path.abspath(getsourcefile(lambda:0)) +testdir = os.path.dirname(current_path) +moduledir = testdir[:testdir.rfind(os.path.sep)] +if moduledir not in sys.path: + sys.path.insert(1,moduledir) + +import pyzdde \ No newline at end of file From 6756db776085c4b90650abf8cfb63609d30c43f2 Mon Sep 17 00:00:00 2001 From: Ralf Hambach Date: Tue, 11 Jul 2017 17:42:21 +0200 Subject: [PATCH 20/23] minor changes to make pyZDDEunittests work - tested with python 2.7, 3.4 - Regexps where too restrictive and did not match floating point numbers on my computer. Use more general regexps suggested from http://docs.python.org/2/library/re.html#simulating-scanf - In some cases, a \ufeff character is found at the beginning of the file. Now removed manually in _readLinesFromFile() - Explicit type casting to int was required in python 3.4 --- pyzdde/zdde.py | 45 ++++++++++++++++++++++++-------------------- pyzdde/zfileutils.py | 6 +++--- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/pyzdde/zdde.py b/pyzdde/zdde.py index 3dab0ae..e41df26 100644 --- a/pyzdde/zdde.py +++ b/pyzdde/zdde.py @@ -7245,62 +7245,65 @@ def zGetPOP(self, settingsFile=None, displayData=False, txtFile=None, # get line list line_list = _readLinesFromFile(_openFile(textFileName)) + # set regexp for parsing float and int data + # see http://docs.python.org/2/library/re.html#simulating-scanf + # note: (?: opens a non-capturing group + pfloat = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?'; # regexp for %e,%E,%f,%g + pint = r'[-+]?(?:0[xX][\dA-Fa-f]+|0[0-7]*|\d+)'; # regexp for %i + # Get data type ... phase or Irradiance? find_irr_data = _getFirstLineOfInterest(line_list, 'POP Irradiance Data', patAtStart=False) data_is_irr = False if find_irr_data is None else True + # Get the Surface number and Grid size grid_line_num = _getFirstLineOfInterest(line_list, 'Grid size') surf_line = line_list[grid_line_num - 1] - surf = int(_re.findall(r'\d{1,4}', surf_line)[0]) # assume: first int num in the line + surf = int(_re.findall(pint, surf_line)[0]) # assume: first int num in the line # is surf number. surf comment can have int or float nums grid_line = line_list[grid_line_num] - grid_x, grid_y = [int(i) for i in _re.findall(r'\d{2,5}', grid_line)] + grid_x, grid_y = [int(i) for i in _re.findall(pint, grid_line)] # Point spacing pts_line = line_list[_getFirstLineOfInterest(line_list, 'Point spacing')] - pat = r'-?\d\.\d{4,6}[Ee][-\+]\d{3}' - pts_x, pts_y = [float(i) for i in _re.findall(pat, pts_line)] + print(_re.findall(pfloat, pts_line)) + pts_x, pts_y = [float(i) for i in _re.findall(pfloat, pts_line)] width_x = pts_x*grid_x width_y = pts_y*grid_y if data_is_irr: # Peak Irradiance and Total Power - pat_i = r'-?\d\.\d{4,6}[Ee][-\+]\d{3}' # pattern for P. Irr, T. Pow, peakIrr, totPow = None, None pi_tp_line = _getFirstLineOfInterest(line_list, 'Peak Irradiance') if pi_tp_line: # Transfer magnitude doesn't have Peak Irradiance info pi_tp_line = line_list[pi_tp_line] pi_info, tp_info = pi_tp_line.split(',') - pi = _re.search(pat_i, pi_info) - tp = _re.search(pat_i, tp_info) + pi = _re.search(pfloat, pi_info) + tp = _re.search(pfloat, tp_info) if pi: peakIrr = float(pi.group()) if tp: totPow = float(tp.group()) else: # Center Phase - pat_p = r'-?\d+\.\d{4,6}' # pattern for Center Phase Info centerPhase = None #cp_line = line_list[_getFirstLineOfInterest(line_list, 'Center Phase')] cp_line = _getFirstLineOfInterest(line_list, 'Center Phase') if cp_line: # Transfer magnitude / Phase doesn't have Center Phase info cp_line = line_list[cp_line] - cp = _re.search(pat_p, cp_line) + cp = _re.search(pfloat, cp_line) if cp: centerPhase = float(cp.group()) # Pilot_size, Pilot_Waist, Pos, Rayleigh [... available for # both Phase and Irr data] - pat_fe = r'\d\.\d{6}' # pattern for fiber efficiency - pat_pi = r'-?\d\.\d{4,6}[Ee][-\+]\d{3}' # pattern for Pilot size/waist pilotSize, pilotWaist, pos, rayleigh = None, None, None, None pilot_line = line_list[_getFirstLineOfInterest(line_list, 'Pilot')] p_size_info, p_waist_info, p_pos_info, p_rayleigh_info = pilot_line.split(',') - p_size = _re.search(pat_pi, p_size_info) - p_waist = _re.search(pat_pi, p_waist_info) - p_pos = _re.search(pat_pi, p_pos_info) - p_rayleigh = _re.search(pat_pi, p_rayleigh_info) + p_size = _re.search(pfloat, p_size_info) + p_waist = _re.search(pfloat, p_waist_info) + p_pos = _re.search(pfloat, p_pos_info) + p_rayleigh = _re.search(pfloat, p_rayleigh_info) if p_size: pilotSize = float(p_size.group()) if p_waist: @@ -7316,9 +7319,9 @@ def zGetPOP(self, settingsFile=None, displayData=False, txtFile=None, if effi_coup_line_num: efficiency_coupling_line = line_list[effi_coup_line_num] efs_info, fer_info, cou_info = efficiency_coupling_line.split(',') - fes = _re.search(pat_fe, efs_info) - fer = _re.search(pat_fe, fer_info) - cou = _re.search(pat_fe, cou_info) + fes = _re.search(pfloat, efs_info) + fer = _re.search(pfloat, fer_info) + cou = _re.search(pfloat, cou_info) if fes: fibEffSys = float(fes.group()) if fer: @@ -7328,8 +7331,7 @@ def zGetPOP(self, settingsFile=None, displayData=False, txtFile=None, if displayData: # Get the 2D data - pat = (r'(-?\d\.\d{4,6}[Ee][-\+]\d{3}\s*)' + r'{{{num}}}' - .format(num=grid_x)) + pat = r'(%s\s*){%s}' % (pfloat,grid_x); start_line = _getFirstLineOfInterest(line_list, pat) powerGrid = _get2DList(line_list, start_line, grid_y) @@ -12177,6 +12179,9 @@ def _readLinesFromFile(fileObj): from the file """ lines = list(_getDecodedLineFromFile(fileObj)) + # remove utf-8 file indicator \ufeff if present + # see https://stackoverflow.com/questions/40397086/python-3-x-about-encoding + lines[0]=lines[0].replace(u'\ufeff',''); return lines def _getFirstLineOfInterest(line_list, pattern, patAtStart=True): diff --git a/pyzdde/zfileutils.py b/pyzdde/zfileutils.py index 1c1b716..806260f 100644 --- a/pyzdde/zfileutils.py +++ b/pyzdde/zfileutils.py @@ -248,8 +248,8 @@ def readZRDFile(file_name, max_segs_per_ray=1000): format_dict = {c_int:'i', c_uint:'I', c_double:'d', c_float:'f'} file_handle = open(file_name, "rb") first_int = read_n_bytes(file_handle, formatChar='i') - zrd_type = _math.floor(first_int/10000) - zrd_version = _math.fmod(first_int,10000) + zrd_type = int(_math.floor(first_int/10000)) + zrd_version = int(_math.fmod(first_int,10000)) max_n_segments = read_n_bytes(file_handle, formatChar='i') if zrd_type == 0: file_type = 'uncompressed' @@ -318,7 +318,7 @@ def writeZRDFile(rayArray, file_name, file_type): c_double, c_float = _ctypes.c_double, _ctypes.c_float format_dict = {c_int:'i', c_uint:'I', c_double:'d', c_float:'f'} file_handle = open(file_name, "wb") - file_handle.write(_pack('i', rayArray[0].zrd_version+zrd_type)) + file_handle.write(_pack('i', rayArray[0].zrd_version+zrd_type)); file_handle.write(_pack('i', rayArray[0].n_segments)) # number of rays for ray in rayArray: file_handle.write(_pack('i', len(ray.status))) # number of segments in the ray From ba4d08a2319449880843ae1f3326ad83c13eafe9 Mon Sep 17 00:00:00 2001 From: Ralf Hambach Date: Tue, 11 Jul 2017 17:44:49 +0200 Subject: [PATCH 21/23] update arraytrace module to run on python 3.4 - Note: some tests in arrayTraceTest.py fail with pyZOS 16.5. Appearently the return values changed for rays that are vignetted. - Note: python 3.6 does not work at all (timeout in DDE link) --- pyzdde/arraytrace/arrayTraceClient.h | 2 +- pyzdde/arraytrace/numpy_interface.py | 40 +++++++++------- pyzdde/arraytrace/raystruct_interface.py | 26 +++++----- test/arrayTraceTest.py | 61 +++++++++++++++++++----- 4 files changed, 87 insertions(+), 42 deletions(-) diff --git a/pyzdde/arraytrace/arrayTraceClient.h b/pyzdde/arraytrace/arrayTraceClient.h index b7f908e..5c59d57 100644 --- a/pyzdde/arraytrace/arrayTraceClient.h +++ b/pyzdde/arraytrace/arrayTraceClient.h @@ -8,7 +8,7 @@ #define DLL_EXPORT __declspec(dllexport) #define WM_USER_INITIATE (WM_USER + 1) -#define DDE_TIMEOUT 1000 +#define DDE_TIMEOUT 1000 // minimal DDE timeout #pragma warning ( disable : 4996 ) // functions like strcpy are now deprecated for security reasons; this disables the warning #pragma comment(lib, "User32.lib") #pragma comment(lib, "gdi32.lib") diff --git a/pyzdde/arraytrace/numpy_interface.py b/pyzdde/arraytrace/numpy_interface.py index 1cbd147..ff58a6f 100644 --- a/pyzdde/arraytrace/numpy_interface.py +++ b/pyzdde/arraytrace/numpy_interface.py @@ -28,8 +28,8 @@ import ctypes as _ct import numpy as _np -if _sys.version_info[0] > 2: - xrange = range +if _sys.version_info[0] < 3: + range = xrange def _is64bit(): """return True if Python version is 64 bit @@ -117,15 +117,18 @@ def zGetTraceArray(hx,hy, px,py, intensity=1., waveNum=1, -------- >>> import numpy as np >>> import matplotlib.pylab as plt + >>> import pyzdde.zdde as pyz + >>> import pyzdde.arraytrace.numpy_interface as nt + >>> ln = pyz.createLink() >>> # cartesian sampling in field an pupil >>> x = np.linspace(-1,1,4) >>> p = np.linspace(-1,1,3) >>> hx,hy,px,py = np.meshgrid(x,x,p,p); >>> # run array-trace - >>> (error,vigcode,pos,dir,normal,intensity) = \\ - zGetTraceArray(hx,hy,px,py); + >>> (error,vigcode,pos,dir,normal,intensity) = nt.zGetTraceArray(hx,hy,px,py); >>> # plot results - >>> plt.scatter(pos[:,0],pos[:,1],c=_np.sqrt(hx**2+hy**2)) + >>> plt.scatter(pos[:,0],pos[:,1],c=np.sqrt(hx**2+hy**2)) + >>> ln.close(); """ # handle input arguments @@ -222,6 +225,7 @@ def zGetTraceDirectArray(startpos, startdir, intensity=1., waveNum=1, >>> import numpy as np >>> import matplotlib.pylab as plt >>> import pyzdde.zdde as pyz + >>> import pyzdde.arraytrace.numpy_interface as nt >>> ln = pyz.createLink() >>> # launch rays from same from off-axis field point >>> # we create initial pos and dir using zGetTraceArray @@ -229,15 +233,15 @@ def zGetTraceDirectArray(startpos, startdir, intensity=1., waveNum=1, >>> startsurf= 1; # in case of collimated input beam >>> lastsurf = ln.zGetNumSurf(); >>> hx,hy,px,py = 0, 0.5, 0, np.linspace(-1,1,nRays); - >>> (_,_,pos,dir,_,_) = zGetTraceArray(hx,hy,px,py,bParaxial=False,surf=startsurf); + >>> (_,_,pos,dir,_,_) = nt.zGetTraceArray(hx,hy,px,py,bParaxial=False,surf=startsurf); >>> # trace ray until last surface >>> points = np.zeros((lastsurf+1,nRays,3)); # indexing: surf,ray,coord >>> z0=0; points[startsurf]=pos; # ray intersection points on starting surface - >>> for isurf in xrange(startsurf,lastsurf): + >>> for isurf in range(startsurf,lastsurf): >>> # trace to next surface - >>> (error,vigcode,pos,dir,_,_)=zGetTraceDirectArray(pos,dir,bParaxial=False,startSurf=isurf,lastSurf=isurf+1); + >>> (error,vigcode,pos,dir,_,_)=nt.zGetTraceDirectArray(pos,dir,bParaxial=False,startSurf=isurf,lastSurf=isurf+1); >>> points[isurf+1]=pos; - >>> points[isurf+1,vigcode<>0]=np.nan; # remove vignetted rays + >>> points[isurf+1,vigcode!=0]=np.nan; # remove vignetted rays >>> # add thickness of current surface (assumes absence of tilts or decenters in system) >>> z0+=ln.zGetThickness(isurf); >>> points[isurf+1,:,2]+=z0; @@ -328,11 +332,14 @@ def zGetOpticalPathDifferenceArray(hx,hy, px,py, waveNum=1,timeout=60000): -------- >>> import numpy as np >>> import matplotlib.pylab as plt + >>> import pyzdde.zdde as pyz + >>> import pyzdde.arraytrace.numpy_interface as nt + >>> ln = pyz.createLink() >>> # pupil sampling along diagonal (px,px) - >>> NP=51; p = _np.linspace(-1,1,NP); - >>> (error,vigcode,opd,pos,dir,intensity) = \\ - zGetOpticalPathDifferenceArray(0,0,p,p); + >>> NP=51; p = np.linspace(-1,1,NP); + >>> (error,vigcode,opd,pos,dir,intensity) = nt.zGetOpticalPathDifferenceArray(0,0,p,p); >>> plt.plot(p,opd[0,0,:]) + >>> ln.close(); """ # handle input arguments @@ -397,7 +404,6 @@ def _test_zGetTraceArray(): print(" number of rays: %d" % len(pos)); if len(pos)<1e5: import matplotlib.pylab as plt - from mpl_toolkits.mplot3d import Axes3D fig = plt.figure() ax = fig.add_subplot(111,projection='3d') ax.scatter(*pos.T,c=_np.sqrt(px**2+py**2)); @@ -417,7 +423,7 @@ def _test_zGetTraceArrayDirect(): # GetTraceArrayDirect ret = zGetTraceDirectArray(startpos,startdir,bParaxial=False,startSurf=0,lastSurf=-1); ret_descr = ('error','vigcode','pos','dir','normal','intensity') - for i in xrange(len(ref)): + for i in range(len(ref)): assert _np.allclose(ref[i],ret[i]), "difference in %s"%ret_descr[i]; def _test_zOPDArray(): @@ -433,7 +439,7 @@ def _test_zOPDArray(): if opd.size<1e5: import matplotlib.pylab as plt plt.figure(); - for f in xrange(NF): + for f in range(NF): plt.plot(px,opd[0,f],label="hx=%5.3f"%hx[f]); plt.legend(loc=0); @@ -452,11 +458,11 @@ def _plot_2D_layout_from_array_trace(): # trace ray until last surface points = _np.zeros((lastsurf+1,nRays,3)); # indexing: surf,ray,coord z0=0; points[startsurf]=pos; # ray intersection points on starting surface - for isurf in xrange(startsurf,lastsurf): + for isurf in range(startsurf,lastsurf): # trace to next surface (error,vigcode,pos,dir,_,_)=zGetTraceDirectArray(pos,dir,bParaxial=False,startSurf=isurf,lastSurf=isurf+1); points[isurf+1]=pos; - points[isurf+1,vigcode<>0]=_np.nan; # remove vignetted rays + points[isurf+1,vigcode!=0]=_np.nan; # remove vignetted rays # add thickness of current surface (assumes absence of tilts or decenters in system) z0+=ln.zGetThickness(isurf); points[isurf+1,:,2]+=z0; diff --git a/pyzdde/arraytrace/raystruct_interface.py b/pyzdde/arraytrace/raystruct_interface.py index ed76244..2c2ca39 100644 --- a/pyzdde/arraytrace/raystruct_interface.py +++ b/pyzdde/arraytrace/raystruct_interface.py @@ -48,8 +48,8 @@ import collections as _co #import gc as _gc -if _sys.version_info[0] > 2: - xrange = range +if _sys.version_info[0] < 3: + range = xrange # Ray data structure as defined in Zemax manual class DdeArrayData(_ct.Structure): @@ -277,7 +277,7 @@ def zGetTraceArray(numRays, hx=None, hy=None, px=None, py=None, intensity=None, else: want_opd = [-1] + [want_opd] * (numRays-1); # fill up the structure - for i in xrange(1, numRays+1): + for i in range(1, numRays+1): rd[i].x = hx[i-1] rd[i].y = hy[i-1] rd[i].z = px[i-1] @@ -301,7 +301,7 @@ def zGetTraceArray(numRays, hx=None, hy=None, px=None, py=None, intensity=None, exec(r + " = [0.0] * numRays", locals(), d) for i in ints: exec(i + " = [0] * numRays", locals(), d) - for i in xrange(1, numRays+1): + for i in range(1, numRays+1): d["x"][i-1] = rd[i].x d["y"][i-1] = rd[i].y d["z"][i-1] = rd[i].z @@ -426,7 +426,7 @@ def zGetTraceDirectArray(numRays, x=None, y=None, z=None, l=None, m=None, waveNum = [1] * numRays # fill up the structure - for i in xrange(1, numRays+1): + for i in range(1, numRays+1): rd[i].x = x[i-1] rd[i].y = y[i-1] rd[i].z = z[i-1] @@ -447,7 +447,7 @@ def zGetTraceDirectArray(numRays, x=None, y=None, z=None, l=None, m=None, exec(r + " = [0.0] * numRays", locals(), d) for i in ints: exec(i + " = [0] * numRays", locals(), d) - for i in xrange(1, numRays+1): + for i in range(1, numRays+1): d["x"][i-1] = rd[i].x d["y"][i-1] = rd[i].y d["z"][i-1] = rd[i].z @@ -605,7 +605,7 @@ def zGetPolTraceArray(numRays, hx=None, hy=None, px=None, py=None, Exr=None, waveNum = [1] * numRays # fill up the structure - for i in xrange(1, numRays+1): + for i in range(1, numRays+1): rd[i].x = hx[i-1] rd[i].y = hy[i-1] rd[i].z = px[i-1] @@ -629,7 +629,7 @@ def zGetPolTraceArray(numRays, hx=None, hy=None, px=None, py=None, Exr=None, exec(r + " = [0.0] * numRays", locals(), d) for i in ints: exec(i + " = [0] * numRays", locals(), d) - for i in xrange(1, numRays+1): + for i in range(1, numRays+1): d["intensity"][i-1] = rd[i].intensity d["Exr"][i-1] = rd[i].Exr d["Exi"][i-1] = rd[i].Exi @@ -793,7 +793,7 @@ def zGetPolTraceDirectArray(numRays, x=None, y=None, z=None, l=None, m=None, waveNum = [1] * numRays # fill up the structure - for i in xrange(1, numRays+1): + for i in range(1, numRays+1): rd[i].x = x[i-1] rd[i].y = y[i-1] rd[i].z = z[i-1] @@ -820,7 +820,7 @@ def zGetPolTraceDirectArray(numRays, x=None, y=None, z=None, l=None, m=None, exec(r + " = [0.0] * numRays", locals(), d) for i in ints: exec(i + " = [0] * numRays", locals(), d) - for i in xrange(1, numRays+1): + for i in range(1, numRays+1): d["intensity"][i-1] = rd[i].intensity d["Exr"][i-1] = rd[i].Exr d["Exi"][i-1] = rd[i].Exi @@ -931,7 +931,7 @@ def zGetNSCTraceArray(x=0.0, y=0.0, z=0.0, l=0.0, m=0.0, n=1.0, Exr=0.0, Exi=0.0 'opl']) nNumRaySegments = rd[0].want_opd # total number of segments stored rayData = ['']*nNumRaySegments - for i in xrange(1, nNumRaySegments+1): + for i in range(1, nNumRaySegments+1): rayData[i-1] = segData(rd[i].wave, rd[i].want_opd, rd[i].vigcode, rd[i].error, rd[i].x, rd[i].y, rd[i].z, rd[i].l, rd[i].m, rd[i].n, rd[i].intensity, rd[i].opd) @@ -986,8 +986,8 @@ def _test_arraytrace_module_basic(): rd = getRayDataArray(nr) # Fill the rest of the ray data array k = 0 - for i in xrange(-10, 11, 1): - for j in xrange(-10, 11, 1): + for i in range(-10, 11, 1): + for j in range(-10, 11, 1): k += 1 rd[k].z = i/20.0 # px rd[k].l = j/20.0 # py diff --git a/test/arrayTraceTest.py b/test/arrayTraceTest.py index c750db9..8b505c9 100644 --- a/test/arrayTraceTest.py +++ b/test/arrayTraceTest.py @@ -11,11 +11,11 @@ #------------------------------------------------------------------------------- from __future__ import division from __future__ import print_function -import os -import sys + import unittest import numpy as np +from _context import pyzdde import pyzdde.zdde as pyz import pyzdde.arraytrace as at import pyzdde.arraytrace.numpy_interface as nt @@ -77,7 +77,7 @@ def compare_array_with_single_trace(self,strace,atrace,param_descr,ret_descr,nr= # Load a lens file into the LDE filename = get_test_file() ret = self.ln.zLoadFile(filename); - if ret<>0: raise IOError("Could not load Zemax file '%s'. Error code %d" % (filename,ret)); + if ret!=0: raise IOError("Could not load Zemax file '%s'. Error code %d" % (filename,ret)); if not self.ln.zPushLensPermission(): raise RuntimeError("Extensions not allowed to push lenses. Please enable in Zemax.") self.ln.zPushLens(1); @@ -88,7 +88,7 @@ def compare_array_with_single_trace(self,strace,atrace,param_descr,ret_descr,nr= aret = atrace(*params) # compare with results from single raytrace - for i in xrange(nr): + for i in range(nr): sret = strace(*params[:,i]); is_close = np.isclose(aret[:,i], np.asarray(sret)); msg = 'array and single raytrace differ for ray #%d:\n' % i; @@ -99,7 +99,7 @@ def compare_array_with_single_trace(self,strace,atrace,param_descr,ret_descr,nr= msg+= '%10s %12g %12g \n'%(ret_descr[j],aret[j,i],sret[j]); self.assertTrue(np.all(is_close), msg=msg); - + @unittest.skip("To be removed") def test_zGetTraceArrayOLd(self): print("\nTEST: arraytrace.zGetTraceArray()") # Load a lens file into the Lde @@ -122,7 +122,7 @@ def test_zGetTraceArrayOLd(self): # compare with results from GetTrace, returns (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) ret_descr = ('error','vigcode','x','y','z','l','m','n','Exr','Eyr','Ezr','intensity') - for i in xrange(nr): + for i in range(nr): reference = self.ln.zGetTrace(w,mode,-1,hx[i],hy[i],px[i],py[i]); is_close = np.isclose(ret[:,i], np.asarray(reference)); msg = 'zGetTraceArray differs from GetTrace for %s ray #%d:\n' % (mode_descr[mode],i); @@ -153,6 +153,7 @@ def atrace(hx,hy,px,py): # perform comparison self.compare_array_with_single_trace(strace,atrace,('hx','hy','px','py'),ret_descr,nr=nr); + @unittest.skip("To be removed") def test_cross_check_zArrayTrace_vs_zGetTraceNumpy_OLD(self): print("\nTEST: comparison of zArrayTrace and zGetTraceNumpy OLD") # Load a lens file into the LDE @@ -164,7 +165,7 @@ def test_cross_check_zArrayTrace_vs_zGetTraceNumpy_OLD(self): rd = at.getRayDataArray(nr) hx,hy,px,py = 2*np.random.rand(4,nr)-1; - for k in xrange(nr): + for k in range(nr): rd[k+1].x = hx[k]; rd[k+1].y = hy[k]; rd[k+1].z = px[k]; @@ -217,16 +218,22 @@ def test_zGetOpticalPathDifference(self): def strace(hx,hy): (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity)=self.ln.zGetTrace(w,0,-1,hx,hy,px,py); # real ray trace to image surface opd=self.ln.zGetOpticalPathDifference(hx,hy,px,py,ref=0,wave=w); # calculate OPD, ref: chief ray - vig=1 if vig<>0 else 0; # vignetting flag is only 0 or 1 in ArrayTrace, not the surface number + vig=1 if vig!=0 else 0; # vignetting flag is only 0 or 1 in ArrayTrace, not the surface number return (error,vig,opd,x,y,z,l,m,n,intensity); # array trace (C-extension), returns (error,vigcode,opd,pos,dir,intensity) def atrace(hx,hy): ret = nt.zGetOpticalPathDifferenceArray(hx,hy,px,py,waveNum=w); - ret = map(lambda a: a.reshape((ret[0].size,-1)), ret); # reshape arguments as (nRays,...) + ret = [ var.reshape((ret[0].size,-1)) for var in ret ]; # reshape each argument as (nRays,...) return np.hstack(ret).T; # perform comparison self.compare_array_with_single_trace(strace,atrace,('hx','hy'),ret_descr); + # ----------------------------------------------------------------------------- + # Test works with Zemax 13 R2 + # Test fails with OpticStudio (ZOS16.5). We obtain different error and vigcodes + # using either single raytrace or arraytrace. Should be handled in the python + # interface to avoid confusion + # ----------------------------------------------------------------------------- def test_zGetTraceDirectNumpy(self): print("\nTEST: arraytrace.numpy_interface.zGetTraceDirectArray()") w = 1; # wavenum @@ -245,12 +252,42 @@ def atrace(x,y,z,l,m): n = np.sqrt(1-0.5*l**2-0.5*m**2); # calculate z-direction pos = np.stack((x,y,z),axis=1); dir = np.stack((l,m,n),axis=1); - ret = nt.zGetTraceDirectArray(pos,dir,bParaxial=mode,startSurf=startSurf,lastSurf=lastSurf); + ret = nt.zGetTraceDirectArray(pos,dir,bParaxial=mode,startSurf=startSurf,lastSurf=lastSurf, + intensity=1,waveNum=w); return np.column_stack(ret).T; # perform comparison self.compare_array_with_single_trace(strace,atrace,('x','y','z','l','m'),ret_descr); # ----------------------------------------------------------------------------- + # Test works with Zemax 13 R2 + # Test fails with OpticStudio (ZOS16.5). We obtain different error and vigcodes + # using either single raytrace or arraytrace. Should be handled in the python + # interface to avoid confusion + # ----------------------------------------------------------------------------- + def test_zGetTraceDirectRaystruct(self): + print("\nTEST: arraytrace.raystruct_interface.zGetTraceDirectArray()") + w = 1; # wavenum + startSurf=0 + lastSurf=-1 + + for mode,descr in [(0,"real"),(1,"paraxial")]: + print(" compare with GetTraceDirect for %s raytrace"% descr); + # single trace (GetTraceDirect), returns (error,vig,x,y,z,l,m,n,l2,m2,n2,intensity) + ret_descr = ('error','vigcode','x','y','z','l','m','n','l2','m2','n2','intensity') + def strace(x,y,z,l,m): + n = np.sqrt(1-0.5*l**2-0.5*m**2); # calculate z-direction (scale l,m to < 1/sqrt(2)) + return self.ln.zGetTraceDirect(w,mode,startSurf,lastSurf,x,y,z,l,m,n); + # array trace (C-extension), returns (error,vigcode,x,y,z,l,m,n,l2,m2,n2,opd,intensity) + def atrace(x,y,z,l,m): + n = np.sqrt(1-0.5*l**2-0.5*m**2); # calculate z-direction + ret = at.zGetTraceDirectArray(x.size,list(x),list(y),list(z),list(l),list(m),list(n), + mode=mode,startSurf=startSurf,lastSurf=lastSurf, + intensity=1,waveNum=w); + mask = np.ones(13,dtype=np.bool); mask[-2]=False; # mask array for removing opd from ret + return np.column_stack(ret).T[mask]; + # perform comparison + self.compare_array_with_single_trace(strace,atrace,('x','y','z','l','m'),ret_descr); + # ----------------------------------------------------------------------------- # test fails for real raytrace, as arrayTrace from Zemax does not return # surface normal correctly. Should be handled in the python interface # to avoid confusion @@ -279,4 +316,6 @@ def atrace(hx,hy,px,py): if __name__ == '__main__': - unittest.main() + # see https://docs.python.org/2/library/unittest.html + unittest.main(module='arrayTraceTest'); + From 40caa6ee65aec9a9c99bf2647618b1d300acce24 Mon Sep 17 00:00:00 2001 From: Ralf Hambach Date: Wed, 12 Jul 2017 11:17:07 +0200 Subject: [PATCH 22/23] remove print statement used for debugging --- pyzdde/zdde.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyzdde/zdde.py b/pyzdde/zdde.py index e41df26..86c4c55 100644 --- a/pyzdde/zdde.py +++ b/pyzdde/zdde.py @@ -7266,7 +7266,6 @@ def zGetPOP(self, settingsFile=None, displayData=False, txtFile=None, # Point spacing pts_line = line_list[_getFirstLineOfInterest(line_list, 'Point spacing')] - print(_re.findall(pfloat, pts_line)) pts_x, pts_y = [float(i) for i in _re.findall(pfloat, pts_line)] width_x = pts_x*grid_x From 9b716711ad66c9a13067e4ac9da3b6415c4d1c6c Mon Sep 17 00:00:00 2001 From: rhambach Date: Thu, 13 Jul 2017 13:45:40 +0200 Subject: [PATCH 23/23] move files from 'Test' to 'test' - Windows folders are not case sensitive, but we had Test and test directory in parallel - remove Test directory from package index (delete __init__.py) --- {test => Test}/_context.py | 0 {test => Test}/arrayTraceTest.py | 2 +- test/__init__.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename {test => Test}/_context.py (100%) rename {test => Test}/arrayTraceTest.py (99%) delete mode 100644 test/__init__.py diff --git a/test/_context.py b/Test/_context.py similarity index 100% rename from test/_context.py rename to Test/_context.py diff --git a/test/arrayTraceTest.py b/Test/arrayTraceTest.py similarity index 99% rename from test/arrayTraceTest.py rename to Test/arrayTraceTest.py index 8b505c9..5f84e2f 100644 --- a/test/arrayTraceTest.py +++ b/Test/arrayTraceTest.py @@ -20,7 +20,7 @@ import pyzdde.arraytrace as at import pyzdde.arraytrace.numpy_interface as nt -from test.pyZDDEunittest import get_test_file +from .pyZDDEunittest import get_test_file class TestArrayTrace(unittest.TestCase): diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000