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 diff --git a/Test/arrayTraceTest.py b/Test/arrayTraceTest.py new file mode 100644 index 0000000..5f84e2f --- /dev/null +++ b/Test/arrayTraceTest.py @@ -0,0 +1,321 @@ +# -*- coding: utf-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. +# For further details, please refer to LICENSE.txt +#------------------------------------------------------------------------------- +from __future__ import division +from __future__ import print_function + +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 + +from .pyZDDEunittest import get_test_file + +class TestArrayTrace(unittest.TestCase): + + @classmethod + 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 + 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 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 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; + 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); + + @unittest.skip("To be removed") + 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.zPushLens(1); + # 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 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); + 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 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); + + @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 + 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; + + for k in range(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 + 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()") + w = 1; # wavenum + + 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 = [ 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 + 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,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, + 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 + # ----------------------------------------------------------------------------- + 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); + + + + +if __name__ == '__main__': + # see https://docs.python.org/2/library/unittest.html + unittest.main(module='arrayTraceTest'); + 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/Release/ArrayTrace.dll b/pyzdde/arraytrace/Release/ArrayTrace.dll deleted file mode 100644 index 1ec5b37..0000000 Binary files a/pyzdde/arraytrace/Release/ArrayTrace.dll and /dev/null differ diff --git a/pyzdde/arraytrace/__init__.py b/pyzdde/arraytrace/__init__.py new file mode 100644 index 0000000..dd5bc80 --- /dev/null +++ b/pyzdde/arraytrace/__init__.py @@ -0,0 +1,3 @@ +__all__ = ['']; +# for backward compatibility +from pyzdde.arraytrace.raystruct_interface import * \ No newline at end of file diff --git a/pyzdde/arraytrace/arrayTraceClient.c b/pyzdde/arraytrace/arrayTraceClient.c index b7a70df..91719a5 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 @@ -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" @@ -20,8 +23,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,11 +46,15 @@ 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; } +/* ---------------------------------------------------------------------------------- + ArrayTrace functions that accept DDERAYDATA as argument + ---------------------------------------------------------------------------------- + */ + int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) { HWND hwnd; /* handle to client window */ @@ -76,7 +82,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 +96,197 @@ int __stdcall arrayTrace(DDERAYDATA * pRAD, unsigned int timeout) return RETVAL; } +/* ---------------------------------------------------------------------------------- + ArrayTrace functions that accept 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 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) +{ + 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=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; i0 ! + 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; @@ -274,7 +471,7 @@ void WaitForData(HWND hwnd) int sleep_count; MSG msg; DWORD dwTime; - dwTime = GetCurrentTime(); + dwTime = GetTickCount(); sleep_count = 0; @@ -284,12 +481,12 @@ 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) + 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 666f0f0..5c59d57 100644 --- a/pyzdde/arraytrace/arrayTraceClient.h +++ b/pyzdde/arraytrace/arrayTraceClient.h @@ -8,8 +8,10 @@ #define DLL_EXPORT __declspec(dllexport) #define WM_USER_INITIATE (WM_USER + 1) -#define DDE_TIMEOUT 50000 +#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") #ifdef __cplusplus extern "C" { @@ -27,7 +29,22 @@ void WaitForData(HWND hwnd); char *GetString(char *szBuffer, int n, char *szSubString); int PostArrayTraceMessage(char *szBuffer, DDERAYDATA *RD); 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 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); +// wrapper for numpy arrays: mode 1 +DLL_EXPORT 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); +// wrapper for numpy arrays: calculate opd +DLL_EXPORT int __stdcall 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); #ifdef __cplusplus } diff --git a/pyzdde/arraytrace/makefile.win b/pyzdde/arraytrace/makefile.win new file mode 100644 index 0000000..a474d0e --- /dev/null +++ b/pyzdde/arraytrace/makefile.win @@ -0,0 +1,112 @@ +#------------------------------------------------------------------------- +# NMAKE-Makefile +#------------------------------------------------------------------------- + +PROJ=ArrayTrace +ARCH=Win32 Release + +#------------------------------------------------------------------------- +# Compiler and Linker +#------------------------------------------------------------------------- + +CC = cl /nologo +LINK = link /nologo +NMAKE= nmake /nologo /C /f Makefile.win + +#------------------------------------------------------------------------- +# Compiler-Flags +#------------------------------------------------------------------------- + +CGFLAGS=/c /MT /D_WINDOWS /D_USRDLL /D_WINDLL /EHsc /W4 /Zp1 +CRFLAGS=/DNDEBUG /Ox +CDFLAGS=/Od /Zi + +#------------------------------------------------------------------------- +# Linker-Flags +#------------------------------------------------------------------------- + +LGFLAGS=/DLL +LRFLAGS=/RELEASE +LDFLAGS=/DEBUG + +#------------------------------------------------------------------------- +# Architecture +#------------------------------------------------------------------------- + +!IF "$(ARCH)" == "Win32 Release" +RELEASEDIR=win32\Release +CFLAGS=$(CGFLAGS) /DWIN32 $(CRFLAGS) +LFLAGS=$(LGFLAGS) $(LRFLAGS) +!ELSEIF "$(ARCH)" == "Win32 Debug" +RELEASEDIR=win32\Debug +CFLAGS=$(CGFLAGS) /DWIN32 $(CDFLAGS) +LFLAGS=$(LGFLAGS) $(LDFLAGS) +!ELSEIF "$(ARCH)" == "x64 Release" +RELEASEDIR=x64\Release +CFLAGS=$(CGFLAGS) $(CRFLAGS) +LFLAGS=$(LGFLAGS) $(LRFLAGS) +!ELSEIF "$(ARCH)" == "x64 Debug" +RELEASEDIR=x64\Debug +CFLAGS=$(CGFLAGS) $(CDFLAGS) +LFLAGS=$(LGFLAGS) $(LDFLAGS) +!ENDIF + + +#------------------------------------------------------------------------- +# Targets +#------------------------------------------------------------------------- + +help: + @echo. + @echo "Syntax : NMAKE ARCH="Win32 Release" target" + @echo. + @echo "Targets : help - Show this text." + @echo " all - release32/64, debug32/64 + @echo " release32 - Creates $(PROJ).DLL for win32 architecture." + @echo " release64 - Creates $(PROJ).DLL for x64 architecture." + @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" + @echo. + +all: debug32 debug64 release32 release64 + @echo. + @echo Done. + +release: clean $(RELEASEDIR) $(RELEASEDIR)\ArrayTrace.dll +release32: + @$(NMAKE) ARCH="Win32 Release" release +release64: + @$(NMAKE) ARCH="x64 Release" release +debug32: + @$(NMAKE) ARCH="Win32 Debug" release +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 + + +#------------------------------------------------------------------------- +# Sub-Targets +#------------------------------------------------------------------------- + +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 new file mode 100644 index 0000000..ff58a6f --- /dev/null +++ b/pyzdde/arraytrace/numpy_interface.py @@ -0,0 +1,483 @@ +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Name: numpy_interface.py +# Purpose: Module for doing array ray tracing in Zemax using numpy arrays +# 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 +#------------------------------------------------------------------------------- +""" +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. zGetOPDArray() + 3. zGetTraceDirectArray() + ToDo: 3. zGetPolTraceArray() + ToDo: 4. zGetPolTraceDirectArray() + ToDo: 5. zGetNSCTraceArray() + +Note +----- +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 +import ctypes as _ct +import numpy as _np + +if _sys.version_info[0] < 3: + range = xrange + +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 +# 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 _init_list1d(var,length,dtype,name): + "repeat variable if it is scalar to become a 1d list of size length" + var=_np.atleast_1d(var); + if var.size==1: + return _np.full(length,var[0],dtype=dtype); + else: + assert var.size==length, 'incompatible length of list \'%s\': should be %d instead of %d'%(name,length,var.size) + return var.flatten(); + + +def zGetTraceArray(hx,hy, px,py, 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) + + Parameters + ---------- + hx,hy : float or vector of length ``numRays`` + list of normalized field heights along x and y axis + px,py : float or vector of length ``numRays`` + 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. + bParaxial : bool, optional + If True, a paraxial raytrace is performed (default: False, real raytrace) + surf : integer, optional + surface to trace the ray to. Usually, the ray data is only needed at + the image surface (``surf = -1``, default) + timeout : integer, optional + command timeout specified in milli-seconds (default: 1min), at least 1s + + Returns + ------- + error : list of integers + * =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 + 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 + 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 + >>> 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) = nt.zGetTraceArray(hx,hy,px,py); + >>> # plot results + >>> plt.scatter(pos[:,0],pos[:,1],c=np.sqrt(hx**2+hy**2)) + >>> ln.close(); + """ + + # handle input arguments + 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); + 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); + + # 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,_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."); + if ret==-998: raise RuntimeError("Timeout reached after %dms"%timeout); + + return (error,vigcode,pos,dir,normal,intensity); + + +def zGetTraceDirectArray(startpos, startdir, intensity=1., waveNum=1, + bParaxial=False, startSurf=0, lastSurf=-1, timeout=60000): + """ + Trace large number of rays defined by their initial position and direction from + one surface to another for the lens file given in the LDE of the main Zemax + application (not in the DDE server). + + Parameters + ---------- + startpos : ndarray of shape (``numRays``,3) + starting point of each ray at the initial surface, given in local coordinates x,y,z + startdir : ndarray of shape (``numRays``,3) + starting direction of each reay at the initial surface, given as + local direction cosines kx,ky,kz + 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. + bParaxial : bool, optional + If True, a paraxial raytrace is performed (default: False, real raytrace) + startSurf : integer, optional + start surface number (default: 0) + lastSurf : integer, optional + surface to trace the ray to. (default: -1, image surface) + timeout : integer, optional + command timeout specified in milli-seconds (default: 1min), at least 1s + + Returns + ------- + error : list of integers + * =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 + 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 + >>> 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 + >>> 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,_,_) = 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 range(startsurf,lastsurf): + >>> # trace to next surface + >>> (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 + >>> # 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 + 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 + ---------- + hx,hy : float or vector of length ``nField`` + list of normalized field heights along x and y axis + 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. + 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 + >>> 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) = nt.zGetOpticalPathDifferenceArray(0,0,p,p); + >>> plt.plot(p,opd[0,0,:]) + >>> ln.close(); + """ + + # handle input arguments + 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); + 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 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,_DBL1D,_DBL1D, _INT,_DBL1D,_DBL1D, _INT,_INT1D, + _INT1D,_INT1D,_DBL1D,_DBL2D,_DBL2D,_DBL1D,_ct.c_uint] + 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.") + 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)); + + +# ########################################################################### +# 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 zGetTraceArray:") + x = _np.linspace(-1,1,4) + p = _np.linspace(-1,1,3) + hx,hy,px,py = _np.meshgrid(x,x,p,p); + (error,vigcode,pos,dir,normal,intensity) = \ + zGetTraceArray(hx,hy,px,py,bParaxial=False,surf=-1); + + print(" number of rays: %d" % len(pos)); + if len(pos)<1e5: + import matplotlib.pylab as plt + fig = plt.figure() + ax = fig.add_subplot(111,projection='3d') + ax.scatter(*pos.T,c=_np.sqrt(px**2+py**2)); + + +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 range(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" + # Basic test of the module functions + 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) = \ + 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 range(NF): + plt.plot(px,opd[0,f],label="hx=%5.3f"%hx[f]); + plt.legend(loc=0); + +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 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 + # 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 diff --git a/pyzdde/arraytrace.py b/pyzdde/arraytrace/raystruct_interface.py similarity index 95% rename from pyzdde/arraytrace.py rename to pyzdde/arraytrace/raystruct_interface.py index 079585c..2c2ca39 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() @@ -22,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 @@ -30,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): @@ -49,16 +67,16 @@ def _is64bit(): """ return _sys.maxsize > 2**31 - 1 -_dllDir = "arraytrace\\x64\\Release\\" if _is64bit() else "arraytrace\\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) +# 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] - + def zArrayTrace(rd, timeout=5000): """function to trace large number of rays on lens file in the LDE of main @@ -71,14 +89,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)) @@ -254,9 +271,13 @@ 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): + 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] @@ -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) @@ -280,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 @@ -405,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] @@ -426,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 @@ -584,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] @@ -608,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 @@ -772,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] @@ -799,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 @@ -910,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) @@ -965,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 @@ -982,8 +1003,10 @@ def _test_arraytrace_module_basic(): for i in range(1, 11): print(rd[k].intensity) print("Success!") + if __name__ == '__main__': # run the test functions _test_getRayDataArray() - _test_arraytrace_module_basic() \ No newline at end of file + _test_arraytrace_module_basic() + \ 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 22ef481..0000000 Binary files a/pyzdde/arraytrace/x64/Release/ArrayTrace.dll and /dev/null differ diff --git a/pyzdde/zdde.py b/pyzdde/zdde.py index 997bd42..86c4c55 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(',') @@ -7245,62 +7245,64 @@ 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)] + 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 +7318,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 +7330,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) @@ -11594,8 +11595,8 @@ def ipzGetLDE(self, num=None): Parameters ---------- - num : integer, optional - if not None, sufaces upto surface number equal to `num` + num : integer, optional + if not None, sufaces upto surface number equal to `num` will be retrieved Returns @@ -11606,8 +11607,8 @@ def ipzGetLDE(self, num=None): ---- Only works in sequential/hybrid mode. Can't retrieve NSC objects. """ - cd = _os.path.dirname(_os.path.realpath(__file__)) - textFileName = cd +"\\"+"prescriptionFile.txt" + fdir = _os.path.dirname(_os.path.realpath(self.zGetFile())) + textFileName = _os.path.join(fdir,"_ipzGetLDE_prescriptionFile.txt") ret = self.zGetTextFile(textFileName,'Pre', "None", 0) assert ret == 0 recSystemData = self.zGetSystem() # Get the current system parameters @@ -12177,6 +12178,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