├── refractive_index.py ├── grating_lumerical.lsf ├── lens_center.py ├── S4conventions.py ├── README.md ├── nearfield_farfield.py ├── design_collimator.py ├── grating.lua ├── nearfield.py └── grating.py /refractive_index.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | (C) 2015 Steven Byrnes 4 | 5 | This is the refractive index data which winds up in grating.lua etc. 6 | """ 7 | import numpy as np 8 | from scipy.interpolate import interp1d 9 | import matplotlib.pyplot as plt 10 | 11 | # Measured by Rob Devlin using ellipsometry. Amorphous TiO2, measured by ALD 12 | nTiO2_data = [ 13 | [300, 3.345145, 0.951696562], 14 | [308, 3.36245201, 0.722775196], 15 | [316, 3.32564183, 0.522696126], 16 | [324, 3.25518632, 0.361115891], 17 | [332, 3.16840055, 0.2377512], 18 | [340, 3.07710975, 0.147551924], 19 | [348, 2.98838525, 0.084265654], 20 | [356, 2.90608198, 0.042124238], 21 | [364, 2.83218365, 0.016378217], 22 | [372, 2.76780314, 0.003307256], 23 | [380, 2.71439342, 0], 24 | [388, 2.67321759, 0], 25 | [396, 2.63934987, 0], 26 | [404, 2.61053712, 0], 27 | [412, 2.58555792, 0], 28 | [420, 2.56361314, 0], 29 | [428, 2.54413697, 0], 30 | [436, 2.52670924, 0], 31 | [444, 2.51100754, 0], 32 | [452, 2.49677828, 0], 33 | [460, 2.48381813, 0], 34 | [468, 2.47196143, 0], 35 | [476, 2.46107142, 0], 36 | [484, 2.45103392, 0], 37 | [492, 2.44175267, 0], 38 | [500, 2.4331458, 0], 39 | [508, 2.42514313, 0], 40 | [516, 2.41768413, 0], 41 | [524, 2.41071625, 0], 42 | [532, 2.40419359, 0], 43 | [540, 2.39807589, 0], 44 | [548, 2.39232765, 0], 45 | [556, 2.38691744, 0], 46 | [564, 2.3818173, 0], 47 | [572, 2.3770023, 0], 48 | [580, 2.37245013, 0], 49 | [588, 2.36814072, 0], 50 | [596, 2.364056, 0], 51 | [604, 2.36017966, 0], 52 | [612, 2.3564969, 0], 53 | [620, 2.35299429, 0], 54 | [628, 2.34965961, 0], 55 | [636, 2.3464817, 0], 56 | [644, 2.34345037, 0], 57 | [652, 2.34055626, 0], 58 | [660, 2.33779081, 0], 59 | [668, 2.33514612, 0], 60 | [676, 2.33261493, 0], 61 | [684, 2.33019053, 0], 62 | [692, 2.32786674, 0], 63 | [700, 2.32563781, 0], 64 | [708, 2.32349844, 0], 65 | [716, 2.32144368, 0], 66 | [724, 2.31946895, 0], 67 | [732, 2.31756998, 0], 68 | [740, 2.3157428, 0], 69 | [748, 2.31398369, 0], 70 | [756, 2.3122892, 0], 71 | [764, 2.31065607, 0], 72 | [772, 2.30908129, 0], 73 | [780, 2.307562, 0], 74 | [788, 2.30609554, 0], 75 | [796, 2.30467941, 0], 76 | [804, 2.30331126, 0], 77 | [812, 2.30198887, 0], 78 | [820, 2.30071017, 0], 79 | [828, 2.29947319, 0], 80 | [836, 2.29827609, 0], 81 | [844, 2.29711711, 0], 82 | [852, 2.29599462, 0], 83 | [860, 2.29490705, 0], 84 | [868, 2.29385293, 0], 85 | [876, 2.29283086, 0], 86 | [884, 2.29183953, 0], 87 | [892, 2.29087768, 0], 88 | [900, 2.28994412, 0], 89 | [908, 2.28903774, 0], 90 | [916, 2.28815745, 0], 91 | [924, 2.28730225, 0], 92 | [932, 2.28647116, 0], 93 | [940, 2.28566327, 0], 94 | [948, 2.28487771, 0], 95 | [956, 2.28411363, 0], 96 | [964, 2.28337025, 0], 97 | [972, 2.28264681, 0], 98 | [980, 2.28194258, 0], 99 | [988, 2.28125689, 0], 100 | [996, 2.28058906, 0], 101 | [1004, 2.27993848, 0], 102 | [1012, 2.27930454, 0], 103 | [1020, 2.27868667, 0], 104 | [1028, 2.27808432, 0], 105 | [1036, 2.27749697, 0], 106 | [1044, 2.27692411, 0], 107 | [1052, 2.27636526, 0], 108 | [1060, 2.27581995, 0], 109 | [1068, 2.27528776, 0], 110 | [1076, 2.27476825, 0], 111 | [1084, 2.27426101, 0], 112 | [1092, 2.27376566, 0], 113 | [1100, 2.27328183, 0], 114 | [1108, 2.27280914, 0], 115 | [1116, 2.27234726, 0], 116 | [1124, 2.27189586, 0], 117 | [1132, 2.27145461, 0], 118 | [1140, 2.27102321, 0], 119 | [1148, 2.27060136, 0], 120 | [1156, 2.27018878, 0], 121 | [1164, 2.26978519, 0], 122 | [1172, 2.26939034, 0], 123 | [1180, 2.26900396, 0], 124 | [1188, 2.26862582, 0], 125 | [1196, 2.26825568, 0], 126 | [1204, 2.26789331, 0], 127 | [1212, 2.26753849, 0], 128 | [1220, 2.26719101, 0], 129 | [1228, 2.26685067, 0], 130 | [1236, 2.26651727, 0], 131 | [1244, 2.26619061, 0], 132 | [1252, 2.26587053, 0], 133 | [1260, 2.26555684, 0], 134 | [1268, 2.26524936, 0], 135 | [1276, 2.26494795, 0], 136 | [1284, 2.26465242, 0], 137 | [1292, 2.26436263, 0], 138 | [1300, 2.26407844, 0], 139 | [1308, 2.26379969, 0], 140 | [1316, 2.26352624, 0], 141 | [1324, 2.26325797, 0], 142 | [1332, 2.26299474, 0], 143 | [1340, 2.26273642, 0], 144 | [1348, 2.26248288, 0], 145 | [1452, 2.25957341, 0], 146 | [1460, 2.25937597, 0], 147 | [1468, 2.25918186, 0], 148 | [1476, 2.25899101, 0], 149 | [1484, 2.25880336, 0], 150 | [1492, 2.25861882, 0], 151 | [1500, 2.25843733, 0], 152 | [1508, 2.25825883, 0], 153 | [1516, 2.25808324, 0], 154 | [1524, 2.25791051, 0], 155 | [1532, 2.25774056, 0], 156 | [1540, 2.25757335, 0], 157 | [1548, 2.25740881, 0], 158 | [1556, 2.25724688, 0], 159 | [1564, 2.25708751, 0], 160 | [1572, 2.25693065, 0], 161 | [1580, 2.25677624, 0], 162 | [1588, 2.25662423, 0], 163 | [1596, 2.25647457, 0], 164 | [1604, 2.25632722, 0], 165 | [1612, 2.25618212, 0], 166 | [1620, 2.25603923, 0], 167 | [1628, 2.25589851, 0], 168 | [1636, 2.2557599, 0], 169 | [1644, 2.25562337, 0], 170 | [1652, 2.25548888, 0], 171 | [1660, 2.25535639, 0], 172 | [1668, 2.25522585, 0], 173 | [1676, 2.25509723, 0], 174 | [1684, 2.25497049, 0], 175 | [1692, 2.25484559, 0], 176 | [1700, 2.2547225, 0]] 177 | 178 | nTiO2 = interp1d([x[0] for x in nTiO2_data], [x[1] + x[2] * 1j for x in nTiO2_data], kind='linear') 179 | 180 | if False: 181 | xs = np.linspace(300, 1700, num=10000) 182 | plt.figure() 183 | plt.plot(xs, nTiO2(xs).real) 184 | plt.plot(xs, nTiO2(xs).imag) 185 | 186 | if False: 187 | for x in [450,500,525,550,575,580,600,625,650]: 188 | assert nTiO2(x).imag == 0 189 | print('TiO2 @', x, 'nm,', round(float(nTiO2(x).real),3)) 190 | 191 | # UV grade fused silica 192 | # from Thorlabs website: 193 | # https://www.thorlabs.com/NewGroupPage9_PF.cfm?Guide=10&Category_ID=155&ObjectGroup_ID=6249 194 | nSiO2 = [ 195 | [450, 1.46554], 196 | [500, 1.462299], 197 | [525, 1.461009], 198 | [550, 1.459883], 199 | [575, 1.458891], 200 | [580, 1.458706], 201 | [600, 1.458009], 202 | [625, 1.457219], 203 | [650, 1.456506]] 204 | 205 | if False: 206 | for x,y in nSiO2: 207 | print('SiO2 @', x, 'nm,', round(y,3)) 208 | -------------------------------------------------------------------------------- /grating_lumerical.lsf: -------------------------------------------------------------------------------- 1 | # (C) 2015 Steven Byrnes 2 | # calculate grating properties 3 | # start with data in temp/xyrr_list0.txt, then proceed to 1, 2, ... 4 | # ends when the file doesn't exist (so be sure to delete old files!) 5 | # This stuff should be set up by export_lumerical() in grating.py 6 | 7 | clear; 8 | 9 | newproject; 10 | 11 | which_grating = -1; 12 | 13 | # infinite while loop 14 | for(0 ; true ; 0) { 15 | 16 | which_grating = which_grating + 1; 17 | 18 | if (fileexists('temp/grating_setup' + num2str(which_grating) + '.txt') == 0) { 19 | exit(2); 20 | } 21 | 22 | 23 | ##### Filenames ###### 24 | 25 | s_from_air_status_filename = 'grating_s_from_air_status' + num2str(which_grating) + '.txt'; 26 | s_from_glass_status_filename = 'grating_s_from_glass_status' + num2str(which_grating) + '.txt'; 27 | 28 | p_from_air_status_filename = 'grating_p_from_air_status' + num2str(which_grating) + '.txt'; 29 | p_from_glass_status_filename = 'grating_p_from_glass_status' + num2str(which_grating) + '.txt'; 30 | 31 | rm(s_from_air_status_filename); 32 | rm(s_from_glass_status_filename); 33 | 34 | rm(p_from_air_status_filename); 35 | rm(p_from_glass_status_filename); 36 | 37 | ##### Fixed parameters ###### 38 | 39 | degree = pi / 180; 40 | # x * degree is "x degrees" (expressed in radians) 41 | # x / degree is "angle x, expressed in degrees" 42 | 43 | temp = readdata('temp/grating_setup' + num2str(which_grating) + '.txt'); 44 | grating_period = temp(1); 45 | lateral_period = temp(2); 46 | angle_in_air = temp(3); #in degrees 47 | n_glass = temp(4); 48 | nTiO2 = temp(5); 49 | thickness = temp(6); 50 | 51 | # setting an index to 0 means "use tabulated data" 52 | # data comes from refractive_index.py 53 | if(nTiO2 == 0) { 54 | f_vs_eps = [c/450e-9, 2.5^2; 55 | c/500e-9, 2.433^2; 56 | c/525e-9, 2.41^2; 57 | c/550e-9, 2.391^2; 58 | c/575e-9, 2.375^2; 59 | c/580e-9, 2.372^2; 60 | c/600e-9, 2.362^2; 61 | c/625e-9, 2.351^2; 62 | c/525e-9, 2.341^2]; 63 | temp = addmaterial("Sampled data"); 64 | setmaterial(temp,"name","tio2"); 65 | setmaterial("tio2","sampled data", f_vs_eps); 66 | } 67 | if(n_glass == 0) { 68 | f_vs_eps = [c/450e-9, 1.466^2; 69 | c/500e-9, 1.462^2; 70 | c/525e-9, 1.461^2; 71 | c/550e-9, 1.46^2; 72 | c/575e-9, 1.459^2; 73 | c/580e-9, 1.459^2; 74 | c/600e-9, 1.458^2; 75 | c/625e-9, 1.457^2; 76 | c/525e-9, 1.457^2]; 77 | temp = addmaterial("Sampled data"); 78 | setmaterial(temp,"name","sio2"); 79 | setmaterial("sio2","sampled data", f_vs_eps); 80 | } 81 | 82 | wavelength=580e-9; 83 | 84 | x_fdtd_min = -grating_period/2; 85 | x_fdtd_max = grating_period/2; 86 | y_fdtd_min = -lateral_period/2; 87 | y_fdtd_max = lateral_period/2; 88 | 89 | z_cyl_bottom = 0; 90 | z_cyl_top = thickness; 91 | 92 | # one of these is the source, other is the monitor 93 | z_glass_side = -400e-9; 94 | z_air_side = thickness + 400e-9; 95 | 96 | z_fdtd_min = -450e-9; 97 | z_fdtd_max = thickness + 450e-9; 98 | 99 | #### Parameters for iterating over multiple runs #### 100 | 101 | # s polarization means Ey nonzero 102 | polarization_is_s_list = [true, false]; 103 | 104 | # from_air = true means the light is coming from air (at angle_in_air) into glass 105 | # fram_air = false means the light is coming from glass (at normal) into air 106 | #from_air_list = [true, false]; 107 | from_air_list = [true]; 108 | 109 | ####### Main loop ######## 110 | 111 | for(i_pol = 1:length(polarization_is_s_list)) { 112 | for(i_fromair = 1:length(from_air_list)) { 113 | 114 | newproject(2); 115 | redrawoff; 116 | 117 | polarization_is_s = polarization_is_s_list(i_pol); 118 | from_air = from_air_list(i_fromair); 119 | 120 | if (polarization_is_s) { pol_str = 's'; } else { pol_str = 'p'; } 121 | if (from_air) { air_str = 'air'; } else { air_str = 'glass'; } 122 | ?'running: ' + pol_str + ' polarization ; light coming from ' + air_str + ' side.'; 123 | 124 | 125 | ########### Set up monitor, substrate, FDTD box, cylinders, source ######## 126 | 127 | # Planar monitor for calculating overall status 128 | addprofile; 129 | set("name", "up"); 130 | set("monitor type","2D Z-normal"); 131 | set("override global monitor settings",true); 132 | set("use linear wavelength spacing",true); 133 | set("frequency points",1); 134 | set("use source limits",true); 135 | if(from_air) { 136 | set("z",z_glass_side); 137 | } else { 138 | set("z", z_air_side); 139 | } 140 | set("x min", x_fdtd_min); 141 | set("x max", x_fdtd_max); 142 | set("y min", y_fdtd_min); 143 | set("y max", y_fdtd_max); 144 | 145 | addrect; 146 | set("name","substrate"); 147 | set("x min", x_fdtd_min); 148 | set("x max", x_fdtd_max); 149 | set("y min", y_fdtd_min); 150 | set("y max", y_fdtd_max); 151 | set("z min", z_fdtd_min); 152 | set("z max", z_cyl_bottom); 153 | if(n_glass != 0) { 154 | set("material",""); 155 | set("index",n_glass); 156 | } else { 157 | set("material","sio2"); 158 | } 159 | 160 | addplane; 161 | set("name","source"); 162 | set("injection axis","z-axis"); 163 | set("x min", x_fdtd_min); 164 | set("x max", x_fdtd_max); 165 | set("y min", y_fdtd_min); 166 | set("y max", y_fdtd_max); 167 | if (from_air) { 168 | set("direction","Backward"); 169 | set("z", z_air_side); 170 | set("angle theta", -angle_in_air); 171 | } else { 172 | set("direction", "Forward"); 173 | set("z", z_glass_side); 174 | set("angle theta", 0); 175 | } 176 | if(polarization_is_s) { 177 | set("polarization angle", 90); 178 | } else { 179 | set("polarization angle", 0); 180 | } 181 | set("wavelength start", wavelength); 182 | set("wavelength stop", wavelength); 183 | 184 | addfdtd; 185 | set("x min", x_fdtd_min); 186 | set("x max", x_fdtd_max); 187 | set("y min", y_fdtd_min); 188 | set("y max", y_fdtd_max); 189 | set("z min", z_fdtd_min); 190 | set("z max", z_fdtd_max); 191 | set("mesh type","auto non-uniform"); 192 | set("mesh accuracy",4); 193 | set("y min bc", "periodic"); 194 | set("y max bc", "periodic"); 195 | if(from_air) { 196 | # Note: Must be 'bloch', not 'periodic', for tilted source 197 | set("x min bc", "bloch"); 198 | set("x max bc", "bloch"); 199 | } else { 200 | set("x min bc", "periodic"); 201 | set("x max bc", "periodic"); 202 | } 203 | # xyrra_list is (x_center, y_center, semi-x-axis, semi-y-axis, CCW rotation) 204 | # (first 4 in microns, last in degrees) 205 | xyrra_list = readdata('temp/grating_xyrra_list' + num2str(which_grating) + '.txt'); 206 | xyrra_list = xyrra_list; 207 | temp = size(xyrra_list); 208 | num_cylinders = temp(1); 209 | for(i = 1:num_cylinders){ 210 | for(shift_x = -1:1){ 211 | for(shift_y = -1:1){ 212 | addcircle; 213 | set("name", 'Cylinder' + num2str(i) + '_' + num2str(shift_x) + '_' + num2str(shift_y)); 214 | set("make ellipsoid", true); 215 | set("radius", xyrra_list(i,3) * 1e-6); 216 | set("radius 2", xyrra_list(i,4) * 1e-6); 217 | set("x", xyrra_list(i,1) * 1e-6 + shift_x * grating_period); 218 | set("y", xyrra_list(i,2) * 1e-6 + shift_y * lateral_period); 219 | set("z min", z_cyl_bottom); 220 | set("z max", z_cyl_top); 221 | if(nTiO2 != 0) { 222 | set("material", ""); 223 | set("index", nTiO2); 224 | } else { 225 | set("material", "tio2"); 226 | } 227 | if(xyrra_list(i,5) != 0) { 228 | set("first axis", "z"); 229 | set("rotation 1", xyrra_list(i,5)); 230 | } 231 | } } } 232 | 233 | 234 | #### Run simulation ##### 235 | 236 | redraw; 237 | save("loop_temp.fsp"); 238 | redrawoff; 239 | run; 240 | redrawoff; 241 | 242 | ### Calculate overall status (performance) ### 243 | 244 | T = transmission("up"); 245 | which_f = 1; 246 | if(from_air) { 247 | outgoing_index = n_glass; 248 | } else { 249 | outgoing_index = 1; 250 | } 251 | grating_power_list = grating("up", which_f, outgoing_index); 252 | #(u1,u2,u3) is the 3D unit vector for a grating order 253 | grating_u1_list = gratingu1("up", which_f, outgoing_index); 254 | grating_u2_list = gratingu2("up", which_f, outgoing_index); 255 | ####### Haven't debugged this part ########## 256 | if(from_air) { 257 | zsample = -1e-3; 258 | E = farfieldexact("up",0,0,zsample,which_f,outgoing_index); 259 | f = getdata("up","f"); 260 | wavelength = c/f; 261 | kvac = 2*pi/wavelength; 262 | propagation_phase_factor = exp(1i * -kvac * outgoing_index * (zsample - z_glass_side)); 263 | Ex = pinch(E,2,1) / propagation_phase_factor; 264 | Ey = pinch(E,2,2) / propagation_phase_factor; 265 | } else { 266 | # Doesn't matter - since I'm optimizing air-to-glass I only need E at the destination 267 | Ex = 0; 268 | Ey = 0; 269 | } 270 | ########## End non-debugged part ############ 271 | 272 | if(polarization_is_s) { 273 | if(from_air) { 274 | filename = s_from_air_status_filename; 275 | } else { 276 | filename = s_from_glass_status_filename; 277 | } 278 | write(filename, num2str(Ey)); 279 | } else { 280 | if(from_air) { 281 | filename = p_from_air_status_filename; 282 | } else { 283 | filename = p_from_glass_status_filename; 284 | } 285 | write(filename, num2str(Ex)); 286 | } 287 | write(filename, num2str(T)); 288 | for(i=1:length(grating_u1_list)) { 289 | for(j=1:length(grating_u2_list)) { 290 | write(filename, num2str([grating_u1_list(i), grating_u2_list(j), grating_power_list(i,j)])); 291 | } } 292 | 293 | }} 294 | 295 | # end of infinite while loop 296 | } 297 | -------------------------------------------------------------------------------- /lens_center.py: -------------------------------------------------------------------------------- 1 | # coding=UTF-8 2 | """ 3 | (C) 2015 Steven Byrnes 4 | 5 | This script defines a HexGridSet object which is used to store a set of 6 | hexagonal grid cylinder patterns. Each unit cell in the center part of 7 | the lens is chosen from this set to create the correct phase. 8 | 9 | """ 10 | 11 | 12 | import math, cmath 13 | import numpy as np 14 | import matplotlib.pyplot as plt 15 | import grating 16 | from numericalunits import nm 17 | pi = math.pi 18 | inf = float('inf') 19 | 20 | degree = pi / 180 21 | 22 | from scipy.interpolate import RegularGridInterpolator 23 | 24 | 25 | class HexGridSet: 26 | """A HexGridSet is a set of geometries for the center of the lens""" 27 | def __init__(self, sep, cyl_height, n_glass=0, n_tio2=0, grating_list=None, 28 | x_amp_list=None, num_entries=20): 29 | """sep is the cylinder nearest-neighbor center-to-center separation. 30 | grating_list is all the relevant configurations in the form of Grating 31 | objects. I know we're not really using them as gratings per se, but the 32 | Grating object still works.""" 33 | self.sep = sep 34 | # nnn_sep is next-nearest-neighbor center-to-center separation 35 | self.nnn_sep = self.sep * 3**0.5 36 | self.cyl_height = cyl_height 37 | self.n_glass = n_glass 38 | self.n_tio2 = n_tio2 39 | if grating_list is not None: 40 | self.grating_list = grating_list 41 | else: 42 | self.grating_list = [] 43 | for diam in np.linspace(100.01*nm, self.sep-100.01*nm, num=num_entries): 44 | # To do someday: Allow radius 0, i.e. leave out a cylinder from some cell 45 | r=diam/2 46 | xyrra_list_in_nm_deg = [[0, 0, r/nm, r/nm, 0], 47 | [self.nnn_sep/2/nm, self.sep/2/nm, r/nm, r/nm, 0]] 48 | g = grating.Grating(grating_period=self.nnn_sep, 49 | lateral_period=self.sep, 50 | n_glass=self.n_glass, 51 | n_tio2=self.n_tio2, 52 | cyl_height=self.cyl_height, 53 | xyrra_list_in_nm_deg=np.array(xyrra_list_in_nm_deg)) 54 | assert grating.validate(g) 55 | self.grating_list.append(g) 56 | if x_amp_list is not None: 57 | self.x_amp_list = np.array(x_amp_list) 58 | 59 | def __repr__(self): 60 | """this is important - after we spend hours finding a nice 61 | GratingCollection, let's say "mycollection", we can just type 62 | "mycollection" or "repr(mycollection)" into IPython and copy the 63 | resulting text. That's the code to re-create that grating collection 64 | immediately, without re-calculating it.""" 65 | if hasattr(self, 'x_amp_list'): 66 | x_amp_list_str = (np.array2string(self.x_amp_list, separator=',') 67 | .replace(' ', '').replace('\n','')) 68 | else: 69 | x_amp_list_str = 'None' 70 | 71 | return ('HexGridSet(' 72 | + 'sep=' + repr(self.sep/nm) + '*nm' 73 | + ', cyl_height=' + repr(self.cyl_height/nm) + '*nm' 74 | + ', n_glass=' + repr(self.n_glass) 75 | + ', n_tio2=' + repr(self.n_tio2) 76 | + ', grating_list= ' + repr(self.grating_list) 77 | + ', x_amp_list=' + x_amp_list_str 78 | + ')') 79 | 80 | def characterize(self, wavelength=580*nm, numG=100, just_normal=True, 81 | shortcut=False, u_steps=3): 82 | """run g.characterize() for each g in the HexGridSet. This stores 83 | far-field amplitude information in the g object. Then also compile 84 | a list of complex p amplitudes. If just_normal is False, use a range 85 | of input angles, not just normal incidence. If shortcut is True, use 86 | ux>=0,uy>=0, and fill in the negative values by symmetry""" 87 | subfolder=grating.random_subfolder_name() 88 | process_list = [] 89 | if just_normal is True: 90 | u_args={'ux_min':0.001, 'ux_max':0.001, 'uy_min':0.001, 'uy_max':0.001, 91 | 'u_steps':1} 92 | elif shortcut is False: 93 | u_args={'ux_min':-0.499, 'ux_max':0.501, 'uy_min':-0.499, 'uy_max':0.501, 94 | 'u_steps':2*u_steps-1} 95 | else: 96 | u_args={'ux_min':0.001, 'ux_max':0.501, 'uy_min':0.001, 'uy_max':0.501, 97 | 'u_steps':u_steps} 98 | for i,g in enumerate(self.grating_list): 99 | process = g.run_lua_initiate(subfolder=subfolder + str(i), 100 | wavelength=wavelength, numG=numG, 101 | **u_args) 102 | process_list.append(process) 103 | for i,g in enumerate(self.grating_list): 104 | g.characterize(process = process_list[i], convert_to_xy=True, 105 | just_normal=just_normal) 106 | grating.remove_subfolder(subfolder+str(i)) 107 | 108 | if (just_normal is False) and (shortcut is True): 109 | assert False # until I double-check that this code is correct, esp. other grating orders 110 | for data in [g.data for g in self.grating_list]: 111 | for e in range(len(data)): 112 | ux,uy,ox,oy,x_or_y = e['ux'],e['uy'],e['ox'],e['oy'],e['x_or_y'] 113 | ampfy,ampfx,ampry,amprx = e['ampfy'],e['ampfx'],e['ampry'],e['amprx'] 114 | assert ux > 0 and uy >= 0 115 | if x_or_y == 'x': 116 | data.append({'ux': -ux, 'uy': uy, 117 | 'ox':-ox, 'oy':oy, 118 | 'x_or_y': x_or_y, 'wavelength_in_nm':e['wavelength_in_nm'], 119 | 'ampfx':ampfx, 'ampfy':ampfy, 120 | 'amprx':amprx, 'ampry':ampry}) 121 | if uy > 0: 122 | data.append({'ux': ux, 'uy': -uy, 123 | 'ox':ox, 'oy':-oy, 124 | 'x_or_y': x_or_y, 'wavelength_in_nm':e['wavelength_in_nm'], 125 | 'ampfx':ampfx, 'ampfy':-ampfy, 126 | 'amprx':amprx, 'ampry':-ampry}) 127 | data.append({'ux': -ux, 'uy':-uy, 128 | 'ox':-ox, 'oy':-oy, 129 | 'x_or_y': x_or_y, 'wavelength_in_nm':e['wavelength_in_nm'], 130 | 'ampfx':ampfx, 'ampfy':-ampfy, 131 | 'amprx':amprx, 'ampry':-ampry}) 132 | else: #x_or_y == 'y' 133 | data.append({'ux': -ux, 'uy': uy, 134 | 'ox':-ox, 'oy':oy, 135 | 'x_or_y': x_or_y, 'wavelength_in_nm':e['wavelength_in_nm'], 136 | 'ampfx':-ampfx, 'ampfy':ampfy, 137 | 'amprx':-amprx, 'ampry':ampry}) 138 | if uy > 0: 139 | data.append({'ux': ux, 'uy': -uy, 140 | 'ox':ox, 'oy':-oy, 141 | 'x_or_y': x_or_y, 'wavelength_in_nm':e['wavelength_in_nm'], 142 | 'ampfx':ampfx, 'ampfy':ampfy, 143 | 'amprx':amprx, 'ampry':ampry}) 144 | data.append({'ux': -ux, 'uy':-uy, 145 | 'ox':-ox, 'oy':-oy, 146 | 'x_or_y': x_or_y, 'wavelength_in_nm':e['wavelength_in_nm'], 147 | 'ampfx':-ampfx, 'ampfy':ampfy, 148 | 'amprx':-amprx, 'ampry':ampry}) 149 | 150 | x_amp_list = [] 151 | for g in self.grating_list: 152 | a = [e for e in g.data if e['x_or_y'] == 'x' 153 | and e['ox']==e['oy']==0 and e['ux']==e['uy']==0.001] 154 | assert len(a) == 1 155 | x_amp_list.append(a[0]['ampfx']) 156 | self.x_amp_list = np.array(x_amp_list) 157 | 158 | def show_properties(self): 159 | d_list = np.array([2 * g.xyrra_list[0,2] for g in self.grating_list]) 160 | x_amp_list = self.x_amp_list 161 | if self.grating_list[0].n_glass == 0: 162 | n_glass = grating.n_glass(self.grating_list[0].data[0]['wavelength_in_nm']) 163 | else: 164 | n_glass = self.grating_list[0].n_glass 165 | fig, ax1 = plt.subplots() 166 | Ts = abs(x_amp_list)**2 / n_glass 167 | phases = np.unwrap(np.angle(x_amp_list)) 168 | ax1.plot(d_list/nm, Ts, 'b') 169 | ax1.set_ylim(0,1) 170 | plt.title('T and phase at normal incidence') 171 | plt.xlabel('diameter') 172 | ax2 = ax1.twinx() 173 | ax2.plot(d_list/nm, phases, 'g') 174 | 175 | def pick_from_phase(self, target_phase): 176 | """given a target phase, find the best Grating object for 177 | achieving this phase. Return its index in self.grating_list""" 178 | if not hasattr(self, 'x_amp_list'): 179 | raise ValueError('Need to run characterize() first') 180 | # I know that I have the right phase convention here (relative to the 181 | # periphery of the lens) because I ran build_nearfield() and plotted 182 | # the complex phase of the nearfield Ex and it was the same in the 183 | # center as the periphery. 184 | fom_list = (self.x_amp_list * np.exp(-1j * target_phase)).imag 185 | best_index = np.argmax(fom_list) 186 | return best_index 187 | 188 | def build_interpolators(self): 189 | """after running self.characterize(), this creates interpolator objects 190 | 191 | If f = self.interpolators[(580,(1,2),'x','ampfy')] 192 | then f([[0.3,0.1,6], [0.4,0.2,3]]) is [X,Y] where X is 193 | the ampfy for light coming at (ux,uy=(0.3,0.1)) onto self.grating_list[6] 194 | at 580nm, x-polarization, (1,2) diffraction order, and Y is likewise 195 | for the next entry""" 196 | if not hasattr(self, 'x_amp_list'): 197 | raise ValueError('Need to run characterize() first') 198 | 199 | self.interpolators = {} 200 | ux_list = sorted({e['ux'] for g in self.grating_list for e in g.data}) 201 | uy_list = sorted({e['uy'] for g in self.grating_list for e in g.data}) 202 | grating_index_list = np.arange(len(self.grating_list)) 203 | for wavelength_in_nm in {round(e['wavelength_in_nm']) for g in self.grating_list for e in g.data}: 204 | for (ox,oy) in {(e['ox'], e['oy']) for g in self.grating_list for e in g.data}: 205 | for x_or_y in ('x','y'): 206 | for amp in ('ampfy','ampfx','ampry','amprx'): 207 | # want grid_data[i,j,k] = amp(ux_list[i], uy_list[j], grating_period_list[k]) 208 | grid_data = np.zeros((len(ux_list), len(uy_list), len(grating_index_list)), 209 | dtype=complex) 210 | for i,ux in enumerate(ux_list): 211 | for j,uy in enumerate(uy_list): 212 | for k, grating_index in enumerate(grating_index_list): 213 | grating_now = self.grating_list[grating_index] 214 | matches = [e for e in grating_now.data 215 | if e['wavelength_in_nm'] == wavelength_in_nm 216 | and (e['ox'], e['oy']) == (ox, oy) 217 | and e['x_or_y'] == x_or_y 218 | and (e['ux'], e['uy']) == (ux,uy)] 219 | assert len(matches) <= 1 220 | if len(matches) == 1: 221 | grid_data[i,j,k] = matches[0][amp] 222 | interp_function = RegularGridInterpolator((ux_list,uy_list,grating_index_list), grid_data) 223 | self.interpolators[(wavelength_in_nm, (ox,oy), x_or_y, amp)] = interp_function 224 | self.interpolator_bounds = (min(ux_list), max(ux_list), min(uy_list), 225 | max(uy_list), min(grating_index_list), 226 | max(grating_index_list)) 227 | 228 | -------------------------------------------------------------------------------- /S4conventions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | (c) 2015 Steven Byrnes 4 | 5 | This script was used to help write and debug the software, it serves no 6 | external purpose. Specifically, S4 software returns complex amplitudes but 7 | doesn't explain well the meaning of the amplitudes. This file is figuring it 8 | out, by checking against the electric and magnetic fields. 9 | 10 | I posted some of what I learned at 11 | https://github.com/victorliu/S4/pull/25/files 12 | 13 | """ 14 | import math 15 | import numpy as np 16 | from numpy import array 17 | import matplotlib.pyplot as plt 18 | import cmath 19 | import grating, gratingdata 20 | from random import random 21 | 22 | 23 | 24 | # my units package 25 | # https://pypi.python.org/pypi/numericalunits 26 | # just run the command "pip install numericalunits" 27 | import numericalunits as nu 28 | from numericalunits import m, nm, um 29 | 30 | pi = math.pi 31 | inf = float('inf') 32 | degree = pi / 180 33 | 34 | def array_almost_equal(a,b): 35 | a,b = np.array(a), np.array(b) 36 | return (abs(a-b)).max() <= 1e-9 * (abs(a) + abs(b)).max() 37 | 38 | def sp_polarization(kx, ky, kz, n): 39 | """If an amplitude in "s" or "p" polarization is 1, find the E and H field 40 | that S4 would output. (...which has H in arbitrary units, E in the same units 41 | but with a factor of Z0 (impedance of free space)). 42 | n is the index.""" 43 | assert n==1 # Haven't worked out the n>1 case 44 | if kx == ky == 0: 45 | # warning: program treats -0.0 differently from 0.0... 46 | Ep = [1,0,0] 47 | Es = [0,1,0] 48 | Hp = [0,1,0] 49 | Hs = [-1,0,0] 50 | return [np.array(v) for v in (Es,Ep,Hs,Hp)] 51 | k = (kx**2 + ky**2 + kz**2)**0.5 52 | Es = [-ky/(kx**2+ky**2)**0.5, kx/(kx**2+ky**2)**0.5, 0] 53 | Ep = [kx*kz/(k * (kx**2+ky**2)**0.5), 54 | ky*kz/(k * (kx**2+ky**2)**0.5), 55 | -(kx**2+ky**2)**0.5 / k] 56 | Es=np.array(Es) ; Ep = np.array(Ep) 57 | 58 | # alternative / derivation 59 | Es_unnorm = np.cross([0,0,1], [kx,ky,kz]) 60 | Ep_unnorm = np.cross(Es_unnorm, [kx,ky,kz]) 61 | Es_alt = Es_unnorm / sum(Es_unnorm**2)**0.5 62 | Ep_alt = Ep_unnorm / sum(Ep_unnorm**2)**0.5 63 | assert array_almost_equal(Es, Es_alt) 64 | assert array_almost_equal(Ep, Ep_alt) 65 | 66 | Hp = Es 67 | Hs = -Ep 68 | return Es,Ep,Hs,Hp 69 | 70 | def xy_polarization(kx,ky,kz, n): 71 | """For output amplitudes, S4 uses a different polarization scheme, which 72 | we'll call 'x' and 'y' polarization. 73 | 74 | There are a couple funny things about it: (1) The unit-amplitude fields are 75 | actually not normalized, (2) The x and y are not orthogonal (except if 76 | kx=0 or ky=0). But it's actually kinda nice in other ways: Mainly, the fact 77 | that it has no discontinuous changes, unlike s and p near normal. So I'm 78 | actually using it myself by choice for incoming waves 79 | (see grating.characterize()). 80 | 81 | Anyway, if an x-polarized or y-polarized amplitude is 1, find the E and H 82 | field that S4 would output. 83 | 84 | n is the index of refraction""" 85 | if kx == ky == 0: 86 | signkz = math.copysign(1,kz) 87 | E_xpol = [signkz/n,0,0] 88 | E_ypol = [0,-signkz/n,0] 89 | H_xpol = [0,1,0] 90 | H_ypol = [1,0,0] 91 | return [np.array(v) for v in (E_xpol,E_ypol,H_xpol,H_ypol)] 92 | 93 | k = (kx**2 + ky**2 + kz**2)**0.5 94 | H_xpol = [0,1,-ky/kz] 95 | E_xpol = [(ky**2+kz**2)/(k*kz*n), -kx*ky/(k*kz*n), -kx/(k*n)] 96 | H_ypol = [1,0,-kx/kz] 97 | E_ypol = [kx*ky/(k*kz*n), (-kx**2-kz**2)/(k*kz*n), ky/(k*n)] 98 | # alternative / derivation 99 | E_xpol_alt = np.cross(H_xpol, [kx/k, ky/k, kz/k]) / n 100 | E_ypol_alt = np.cross(H_ypol, [kx/k, ky/k, kz/k]) / n 101 | assert array_almost_equal(E_xpol, E_xpol_alt) 102 | assert array_almost_equal(E_ypol, E_ypol_alt) 103 | return [np.array(v) for v in (E_xpol,E_ypol,H_xpol,H_ypol)] 104 | 105 | def x_from_sp(kx,ky,kz,n): 106 | """how to create incoming x polarization from combining s and p. Just 107 | checking my math here""" 108 | assert n==1 109 | Es,Ep,Hs,Hp = sp_polarization(kx,ky,kz,n=n) 110 | E_xpol,E_ypol,H_xpol,H_ypol = xy_polarization(kx,ky,kz,n=n) 111 | k = (kx**2 + ky**2 + kz**2)**0.5 112 | p_coef = kx/(kx**2+ky**2)**0.5 113 | s_coef = -ky*k/(kz*(kx**2+ky**2)**0.5) 114 | assert array_almost_equal(p_coef*Hp + s_coef*Hs, H_xpol) 115 | assert array_almost_equal(p_coef*Ep + s_coef*Es, E_xpol) 116 | 117 | def y_from_sp(kx,ky,kz,n): 118 | """how to create incoming y polarization by combining s and p. Just 119 | checking my math here""" 120 | assert n==1 121 | Es,Ep,Hs,Hp = sp_polarization(kx,ky,kz,n=n) 122 | E_xpol,E_ypol,H_xpol,H_ypol = xy_polarization(kx,ky,kz,n=n) 123 | k = (kx**2 + ky**2 + kz**2)**0.5 124 | p_coef = -ky/(kx**2+ky**2)**0.5 125 | s_coef = -kx*k/(kz*(kx**2+ky**2)**0.5) 126 | assert array_almost_equal(p_coef*Hp + s_coef*Hs, H_ypol) 127 | assert array_almost_equal(p_coef*Ep + s_coef*Es, E_ypol) 128 | 129 | def arbitrary_from_xy(Hx,Hy,kx,ky,kz,n): 130 | """how to recreate an arbitrary field by combing x and y polarizations. 131 | Just checking my math here. Oh, actually this is obvious.""" 132 | E_xpol,E_ypol,H_xpol,H_ypol = xy_polarization(kx,ky,kz,n=n) 133 | x_coef = Hy 134 | y_coef = Hx 135 | assert array_almost_equal((x_coef*H_xpol + y_coef*H_ypol)[0:2], [Hx,Hy]) 136 | 137 | x_from_sp(random(),random(),random(),n=1) 138 | y_from_sp(random(),random(),random(),n=1) 139 | arbitrary_from_xy(random(),random(),random(), random(), random(),n=random()) 140 | 141 | 142 | """The rest of the file is for making S4 output (1) Complex amplitudes of 143 | propagating diffraction orders, and (2) Actual E and H fields at particular 144 | points. Then find the formula for calculating (2) given (1).""" 145 | 146 | 147 | 148 | def read_fields(mygrating, target_wavelength=580*nm): 149 | """When grating.lua is set up to output the complex amplitudes and then 150 | real-space fields, this function is for parsing that output. 151 | Again, this function will not work if you don't uncomment certain lines 152 | in grating.lua.""" 153 | # Assuming lua is set up to spit out near-field... 154 | ux = math.sin(mygrating.get_angle_in_air(580*nm)) - 0.1 155 | uy = 0.123 156 | process = mygrating.run_lua_initiate(ux_min=ux, ux_max=ux, uy_min=uy, 157 | uy_max=uy, u_steps=1, wavelength=600*nm) 158 | output, error = process.communicate() 159 | output = output.decode() 160 | 161 | grating_amplitude_data = [] 162 | lines = output.split('\r\n') 163 | for i,line in enumerate(lines): 164 | if line[0:6] == 'Fields': 165 | break 166 | split = line.split() 167 | #print(split) 168 | if len(split) > 0: 169 | grating_amplitude_data.append({'wavelength': float(split[0]), 170 | 's_or_p': split[1], 171 | 'ux':float(split[2]), 172 | 'uy':float(split[3]), 173 | 'ox':int(split[4]), 174 | 'oy':int(split[5]), 175 | 'ampfy':float(split[6]) + 1j * float(split[7]), 176 | 'ampfx':float(split[8]) + 1j * float(split[9]), 177 | 'ampry':float(split[10]) + 1j * float(split[11]), 178 | 'amprx':float(split[12]) + 1j * float(split[13])}) 179 | field_data = [] 180 | for line in lines[i+1:]: 181 | if line != '': 182 | field_data.append([float(x) for x in line.split('\t')]) 183 | field_data = array(field_data) 184 | x_list = np.unique(field_data[:,0]) * um 185 | y_list = np.unique(field_data[:,1]) * um 186 | z = field_data[0,2] * um 187 | assert (field_data[:,2] * um == z).all() 188 | # E[ix, iy, 2] is Ez at the point x_list[ix],y_list[iy] 189 | E = np.empty(shape=(len(x_list), len(y_list),3), dtype=complex) 190 | H = np.empty(shape=(len(x_list), len(y_list),3), dtype=complex) 191 | for row in field_data: 192 | ix = np.argmin(abs(x_list/um - row[0])) 193 | iy = np.argmin(abs(y_list/um - row[1])) 194 | E[ix,iy,0] = row[3] + 1j * row[9] 195 | E[ix,iy,1] = row[4] + 1j * row[10] 196 | E[ix,iy,2] = row[5] + 1j * row[11] 197 | H[ix,iy,0] = row[6] + 1j * row[12] 198 | H[ix,iy,1] = row[7] + 1j * row[13] 199 | H[ix,iy,2] = row[8] + 1j * row[14] 200 | 201 | return E,H,x_list,y_list,z,grating_amplitude_data 202 | 203 | 204 | def E_from_amplitudes(x, y, z, grating_amplitude_list, mygrating): 205 | """z is relative to air-cylinder interface, i.e. the start of the first S4 206 | layer. 207 | 208 | Note, you need to set pol by hand in the first line to agree with 209 | grating.lua""" 210 | 211 | pol = 's' # TODO - read from lua. For now, set it by hand. 212 | assert len({x['wavelength'] for x in grating_amplitude_list}) == 1 213 | wavelength_in_nm = grating_amplitude_list[0]['wavelength'] 214 | output_amplitude_list = [e for e in grating_amplitude_list if e['s_or_p'] == pol] 215 | #num_orders = len(output_amplitude_list) 216 | z_above_cyl = z - mygrating.cyl_height 217 | # not interested in evanescent crap 218 | assert z_above_cyl > 3*um or z < -3*um 219 | kvac = 2*pi / (wavelength_in_nm * nm) 220 | n_glass = mygrating.n_glass if mygrating.n_glass > 0 else grating.n_glass(wavelength_in_nm) 221 | kglass = kvac * n_glass 222 | 223 | E = np.array([0,0,0], dtype=complex) 224 | H = np.array([0,0,0], dtype=complex) 225 | 226 | ktotal = kglass if z > 0 else kvac 227 | kz_sign = +1 if z > 0 else -1 228 | for d in output_amplitude_list: 229 | ux,uy,ox,oy,ampfy,ampfx,ampry,amprx = d['ux'],d['uy'],d['ox'],d['oy'],d['ampfy'],d['ampfx'],d['ampry'],d['amprx'] 230 | kx_incoming = ux * kvac 231 | ky_incoming = uy * kvac 232 | kx = kx_incoming + mygrating.grating_kx * ox 233 | ky = ky_incoming + 2*pi/mygrating.lateral_period * oy 234 | kz = kz_sign * (ktotal**2 - kx**2 - ky**2)**0.5 235 | if kz.imag != 0: 236 | #the TIR-type orders are propagaing when z>0 but evanescent in air 237 | assert z<0 238 | continue 239 | if z > 0: 240 | E_fx,E_fy,H_fx,H_fy = xy_polarization(kx,ky,kz,n_glass) 241 | 242 | E += ((ampfy * E_fy 243 | + ampfx * E_fx) 244 | * cmath.exp(1j * kx * x) 245 | * cmath.exp(1j * ky * y) 246 | * cmath.exp(1j * kz * z_above_cyl)) 247 | H += ((ampfy * H_fy 248 | + ampfx * H_fx) 249 | * cmath.exp(1j * kx * x) 250 | * cmath.exp(1j * ky * y) 251 | * cmath.exp(1j * kz * z_above_cyl)) 252 | else: 253 | E_rx,E_ry,H_rx,H_ry = xy_polarization(kx,ky,kz,1) 254 | 255 | E += ((ampry * E_ry 256 | + amprx * E_rx) 257 | * cmath.exp(1j * kx * x) 258 | * cmath.exp(1j * ky * y) 259 | * cmath.exp(1j * kz * z)) 260 | H += ((ampry * H_ry 261 | + amprx * H_rx) 262 | * cmath.exp(1j * kx * x) 263 | * cmath.exp(1j * ky * y) 264 | * cmath.exp(1j * kz * z)) 265 | if z<0: 266 | kx = kx_incoming 267 | ky = ky_incoming 268 | kz = (ktotal**2 - kx**2 - ky**2)**0.5 269 | assert kz.imag == 0 270 | Es,Ep,Hs,Hp = sp_polarization(kx,ky,kz,1) 271 | if pol == 's': 272 | amplitude_s = 1 273 | amplitude_p = 0 274 | else: 275 | amplitude_s = 0 276 | amplitude_p = 1 277 | 278 | E += ((amplitude_s * Es 279 | + amplitude_p * Ep) 280 | * cmath.exp(1j * kx * x) 281 | * cmath.exp(1j * ky * y) 282 | * cmath.exp(1j * kz * z)) 283 | H += ((amplitude_s * Hs 284 | + amplitude_p * Hp) 285 | * cmath.exp(1j * kx * x) 286 | * cmath.exp(1j * ky * y) 287 | * cmath.exp(1j * kz * z)) 288 | 289 | 290 | return E,H 291 | 292 | 293 | ########## Test ####### 294 | 295 | mygrating = gratingdata.mygrating31a 296 | #mygrating = gratingdata.mygrating45n 297 | mygrating.lateral_period *= 2.7 298 | #mygrating.n_glass = 1.2 299 | E,H,x_list,y_list,z,grating_amplitude_data = read_fields(mygrating, target_wavelength=600*nm) 300 | print('z/um', z/um) 301 | 302 | ix = 16 303 | iy = 12 304 | x = x_list[ix] 305 | y = y_list[iy] 306 | 307 | E_from_amps, H_from_amps = E_from_amplitudes(x,y,z,grating_amplitude_data,mygrating) 308 | print('Hopefully all of the following are equal to 1.0...') 309 | print('E ratio x', E_from_amps[0] / E[ix,iy,0]) 310 | print('E ratio y', E_from_amps[1] / E[ix,iy,1]) 311 | print('E ratio z', E_from_amps[2] / E[ix,iy,2]) 312 | print('H ratio x', H_from_amps[0] / H[ix,iy,0]) 313 | print('H ratio y', H_from_amps[1] / H[ix,iy,1]) 314 | print('H ratio z', H_from_amps[2] / H[ix,iy,2]) 315 | 316 | 317 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | (C) 2015-2017 Steven Byrnes. This is free software, released under the MIT license - see bottom of this README. 2 | 3 | This is software for designing, optimizing, and simulating metasurface lenses (and metasurface beam deflectors). It was written for the purpose of doing calculations described the paper "Designing large, high-efficiency, high-numerical-aperture, transmissive meta-lenses for visible light" at https://doi.org/10.1364/OE.24.005110 Please refer to that paper for motivation and method. I thank the coauthors of that paper for valuable help and feedback, and I thank Osram and Draper for support. 4 | 5 | Set up and requirements 6 | ======================= 7 | 8 | The Python code is for Python 3.4 or later. The easiest way to install Python is Anaconda: https://www.anaconda.com/download 9 | 10 | The actual electromagnetic simulation is done using S4, a rigorous coupled-wave analysis (RCWA) code. You need to download S4.exe from http://web.stanford.edu/group/fan/S4/install.html . (UPDATE: Oops, the windows binary file S4.exe is no longer available from that website. I don’t know why. Luckily, I still have the old version of S4.exe that I used myself when writing my paper, and I have posted it at https://sjbyrnes.com/S4.exe .) The program expects `grating.py`, `grating.lua`, `S4.exe`, etc. to all be in the same folder. Then there should be a subfolder called `temp`. The program writes configuration files (for communicating between python and S4) into `temp` and into subfolders of `temp`. I set it up that way so that I could remove `temp` from dropbox. (The config files get changed by the program gizillions of times per minute, so it's silly for dropbox to synchronize them.) 11 | 12 | The code is currently windows-only. I'm sure it can be easily modified for mac by downloading the mac version of S4 and changing the subprocess commands etc. It could be modified for linux in theory, but not really in practice, because the S4 website does not provide pre-compiled binaries for linux. You need to compile it yourself. The code has makefiles and instructions for compiling, but I guess the instructions must be full of mistakes, or very tricky. I could not get it to work, and other people have had problems too (see https://github.com/victorliu/S4/issues ). I mean, you're welcome to try, but if you can use the pre-compiled versions you'll potentially save a lot of time. 13 | 14 | Using the code 15 | ============== 16 | 17 | Now, to design a metasurface: 18 | 19 | The first step is designing a set of individual "gratings" (i.e. unit cells of metasurface beam deflectors). The code is in `grating.py`. (Which in turn calls `grating.lua`, the script that directly interfaces with S4.) Create a Grating object however you want, then make it better by running `optimize()` and/or `optimize2()`. Read `grating.lua` for the details of what precise figure-of-merit you're optimizing, particularly what wavelength(s) and polarizations. Note that `optimize()` and `optimize2()` are very basic optimization algorithms; lots of room for improvement. 20 | 21 | Anyway, now you have some optimized `Grating`s. Run `vary_angle()` (still in `grating.py`) to use this as the starting point for a `GratingCollection`, a group of gradually-varying gratings. You might need a few `GratingCollection`s to fill out a complete lens, especially at high numerical aperture. `vary_angle()` runs from the inside of the lens towards the outside, so if you want a `GratingCollection` that runs from incident angle=20° to incident angle=30°, you start with a grating designed for incident angle=20°, not 30°. 22 | 23 | One other ingredient is the center of the lens. If you go to `lens_center.py`, you'll find code to make a `HexGridSet`, i.e. a set of `Grating` objects corresponding to simple periodic nano-pillar structures on a hexagonal grid. The set consists of different choices of diameter for the pillars. Despite the name (`Grating`), these should generally be spaced closely enough that there are no extra propagating diffraction orders. If you run `hgs.characterize(...)` where `hgs` is a `HexGridSet`, then it will calculate and store the phase associated with different diameters, and then you know which pillar to put at which location. 24 | 25 | With both the `HexGridSet` for the center, and the `GratingCollection`s for the periphery, we put them together using `design_collimator.py`. This has functions to design a lens and store the design in a compact form, and also to create a list of nano-pillars for the whole lens, or even create DXF and SVG files of the design. 26 | 27 | Next, you probably want to calculate the far-field. Run `gc.characterize(...)` for each `GratingCollection` `gc`, and also for the `HexGridSet`. This builds up a database of complex diffraction amplitudes as a function of incident angle, polarization, wavelength, and diffraction order. Run `gc.build_interpolators()` to create interpolating functions related to this database. Then finally use `nearfield.py` to calculate the near-field based on that database, and then `nearfield_farfield.py` to do a nearfield-farfield transform. 28 | 29 | Note on saving your work 30 | ======================== 31 | 32 | Any time you find a good Grating or GratingCollection or HexGridSet that you want to remember for later, let's say `x`, then run `print(x)` (or type `x` or in your ipython console) to get a specification of this object, in the form of a string of python code which recreates it. Then using copy-and-paste, you can make a python script file that recreates this grating instantaneously. So you don't have to start from scratch each time. 33 | 34 | Note that the specification of a `GratingCollection` may be many megabytes long, particularly if you have run `characterize()` on it. (The `characterize()` data is saved. The interpolators are not saved, but it only takes a second to re-create them.) Anyway it's enough code that some syntax-highlighting text editors (like the one built into Spyder) will take hours to open the file. Sublime handles it pretty well, as does notepad++. 35 | 36 | List of files 37 | ============= 38 | 39 | grating.py, lens_center.py, grating.lua, design_collimator.py, nearfield.py, nearfield_farfield.py were mentioned above. 40 | 41 | grating_lumerical.lsf is a lumerical script file. It simulates the same gratings in lumerical for comparison with S4. (Use grating.py --> run_lumerical()). So far I've found they're pretty consistent, within 5 or 10% absolute efficiency. Warning: This probably doesn't work at the moment, because I changed some function definitions elsewhere in the code recently but didn't re-check this part. 42 | 43 | S4conventions.py is some tests to understand the exact definition of "output amplitude" in S4, i.e. all the phase conventions, sign conventions, polarization conventions, etc. To run this you need to uncomment some lines in grating.lua. 44 | 45 | refractive_index.py is just a little script where refractive index data for TiO2 and glass is stored. 46 | 47 | Getting-started example (but the discussion above is more comprehensive) 48 | ======================================================================== 49 | 50 | **One-time setup:** Clone this repository, then download S4.exe into that same directory (see above), and also add an empty subfolder to that directory called "temp". 51 | 52 | **Now open `grating.lua`:** What aspect of the gratings is it set to optimize? This is set mainly in the `display_fom` ("display the figure of merit") function. As of this writing, it is set to optimize a grating that sends 0.580μm light into the -1 order and 0.450μm light into the 0 order. That's random, it's just the last thing I wanted to calculate before I uploaded this file. You probably want to do something different. So edit it! (Other example figures-of-merit are commented out nearby.) The script is in Lua, which is an extremely simple programming language (if you're not sure what a command does, just google-search it), and the S4-specific commands are explained at http://web.stanford.edu/group/fan/S4/lua_api.html . Note that there are default refractive indices for glass and TiO2 in little lists at the top, but they only work for the specific wavelengths on the list; if you're interested in other wavelengths or materials you need to edit that code, or input the refractive indices in Python. Let's say in this example that I'm only interested in 785nm light, so I make some edits to the file to use the 785nm refractive indices and calculate the grating efficiency for 785nm. (Exact edits not shown.) 53 | 54 | **Now open Spyder (or other python IDE) and do the following:** 55 | 56 | ```python 57 | # First run grating.py in your ipython console. 58 | # Then in the same console: 59 | from numericalunits import nm 60 | from numpy import array 61 | from math import pi 62 | degree = pi/180 63 | # This example grating has two nanopillars per period. 64 | # I picked the starting parameters arbitrarily. Note that this particular 65 | # example will end up working very inefficiently; I'm just illustrating the 66 | # commands here. 67 | mygrating=Grating(lateral_period=560*nm, cyl_height=500*nm, 68 | target_wavelength=785*nm, angle_in_air=65*degree, 69 | xyrra_list_in_nm_deg=array([[0., 0., 200., 150., 0.], 70 | [400., 280., 150., 200., 10.]])) 71 | optimize(mygrating, target_wavelength=785*nm) 72 | ``` 73 | 74 | ...this runs and runs, and I can either let it complete, or maybe after a while decide the structure is good enough. Then I can copy-paste the latest configuration from the console output (editing the variable name): 75 | 76 | ```python 77 | mygrating_optimized=Grating(lateral_period=560.0*nm, grating_period=866.1516663855559*nm, cyl_height=500.0*nm, n_glass=0, n_tio2=0, xyrra_list_in_nm_deg=np.array([[-2.15000000e+02,2.00000000e+00,2.44000000e+02,1.11000000e+02,-1.52666625e-13],[1.96000000e+02,-2.78000000e+02,1.47000000e+02,2.30000000e+02,1.00000000e-01]]), data=None) 78 | ``` 79 | 80 | (Maybe I'll also save this line in a separate text file for safekeeping.) 81 | 82 | To see what this looks like: 83 | 84 | ```python 85 | mygrating_optimized.show_config() 86 | ``` 87 | 88 | Now let's say I want a family of smoothly-varying gratings designed for incident angle spanning the range from 65° to 70°, to be tiled into a round (as opposed to cylindrical) lens. I can run: 89 | 90 | ```python 91 | vary_angle(start_grating=mygrating_optimized, end_angle=70*degree, lens_type='round', target_wavelength=785*nm) 92 | ``` 93 | 94 | This will run for quite a while, and eventually print a `GratingCollection` object. Again, I copy-paste from the console output into a variable name of my choice, and probably also copy it into a separate text file for safekeeping: 95 | 96 | ```python 97 | my_gratingcollection_65_to_70 = GratingCollection(target_wavelength=785.0*nm, lateral_period=261.13228856679905*nm, lens_type='round', grating_list= [Grating(lateral_period=565.6*nm, grating_period=864.6262219924635*nm, cyl_height=500.0*nm, n_glass=0, n_tio2=0, xyrra_list_in_nm_deg=np.array([[-2.15907032e+02,2.01782243e+00,2.45133589e+02,1.11028705e+02,1.87512868e-02],[1.91953580e+02,-2.80871711e+02,1.46444938e+02,2.30320164e+02,4.59054597e-02]]), data=None), Grating(lateral_period=560.0*nm, grating_period=866.1516663855559*nm, cyl_height=500.0*nm, n_glass=0, n_tio2=0, xyrra_list_in_nm_deg=np.array([[-2.15000000e+02,2.00000000e+00,2.44000000e+02,1.11000000e+02,-1.52666625e-13],[1.96000000e+02,-2.78000000e+02,1.47000000e+02,2.30000000e+02,1.00000000e-01]]), data=None), ... etc. etc. etc.]) 98 | ``` 99 | 100 | If I want to later calculate a lens far-field incorporating this `GratingCollection`, I need to characterize its transmission properties using 101 | 102 | ```python 103 | my_gratingcollection_65_to_70.characterize(785*nm, numG=30) 104 | ``` 105 | 106 | (`numG` controls the speed and accuracy of the RCWA, I lowered it here because I'm in a hurry.) 107 | 108 | Now `my_gratingcollection_65_to_70` has its diffraction efficiency data stored in it. I type into the console `my_gratingcollection_65_to_70` or `print(my_gratingcollection_65_to_70)`, and again probably save the results in a separate text-file for safekeeping, so I don't need to repeat the time-consuming calculation next time. Note that this command may be very long, see "Note on saving your work" above. 109 | 110 | OK, hopefully that's enough to get you started; you can email me with further questions, or if you expand this tutorial yourself and send it to me, I'll post it for everyone else's benefit. 111 | 112 | License 113 | ======= 114 | 115 | Copyright (c) 2015-2017 Steven Byrnes 116 | 117 | Permission is hereby granted, free of charge, to any person obtaining a copy 118 | of this software and associated documentation files (the "Software"), to deal 119 | in the Software without restriction, including without limitation the rights 120 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 121 | copies of the Software, and to permit persons to whom the Software is 122 | furnished to do so, subject to the following conditions: 123 | 124 | The above copyright notice and this permission notice shall be included in all 125 | copies or substantial portions of the Software. 126 | 127 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 128 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 129 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 130 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 131 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 132 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 133 | SOFTWARE. 134 | -------------------------------------------------------------------------------- /nearfield_farfield.py: -------------------------------------------------------------------------------- 1 | # coding=UTF-8 2 | """ 3 | (C) 2015 Steven Byrnes 4 | 5 | Nearfield-farfield transform 6 | """ 7 | import numpy as np 8 | from numpy.fft import fft2, fftshift 9 | from math import pi 10 | import numericalunits as nu 11 | import matplotlib.pyplot as plt 12 | 13 | 14 | def farfield_from_nearfield(fftEx, fftEy, fftHx, fftHy, xp_list, yp_list, wavelength, n_glass): 15 | """following Taflove 1995. 16 | xp, yp (really x', y', p stands for "prime") are coordinates on the lens, 17 | with xp=yp=0 at the lens center 18 | fftEx is fft2(fftshift(Ex)) and ditto with the others. I'm doing it this 19 | way rather than FFTing inside the function so that the real-space data can 20 | be more easily deleted to reclaim precious RAM""" 21 | 22 | dxp = xp_list[1] - xp_list[0] 23 | dyp = yp_list[1] - yp_list[0] 24 | num_x = len(xp_list) 25 | num_y = len(yp_list) 26 | assert fftEx.shape == fftEy.shape == fftHx.shape == fftHy.shape == (num_x, num_y) 27 | for l in [xp_list,yp_list]: 28 | diffs = [l[i+1] - l[i] for i in range(len(l)-1)] 29 | assert 0 < diffs[0] < wavelength/2 30 | assert max(diffs) - min(diffs) <= 1e-9 * max(abs(d) for d in diffs) 31 | 32 | # these formulas are justified in farfield_from_nearfield_helper() 33 | # Note that ux,uy are the x- and y- direction cosines (components of the 34 | # unit vector in the propagation direction) ... in glass not air 35 | ux_list = np.arange(num_x) * (wavelength/n_glass) / (dxp * num_x) 36 | uy_list = np.arange(num_y) * (wavelength/n_glass) / (dyp * num_y) 37 | # pick the correctly aliased version 38 | ux_list[ux_list > ux_list.max()/2] -= (wavelength/n_glass) / dxp 39 | uy_list[uy_list > uy_list.max()/2] -= (wavelength/n_glass) / dyp 40 | 41 | # To conserve RAM we run the rest of the calculation in chunks, using 42 | # farfield_from_nearfield_helper(), so that intermediate results can be 43 | # discarded 44 | 45 | pts_at_a_time = 1e7 46 | uy_pts_at_a_time = int(pts_at_a_time / ux_list.size) 47 | 48 | P_here_times_r2_over_uz = np.zeros(shape=(ux_list.size,uy_list.size), dtype=float) 49 | 50 | start = 0 51 | end = min(start+uy_pts_at_a_time, uy_list.size) 52 | while start < uy_list.size: 53 | print('running uy-index', start, 'to', end, 'out of', uy_list.size) 54 | uy_list_now = uy_list[start:end] 55 | 56 | temp = farfield_from_nearfield_helper(fftEx=fftEx[:,start:end], 57 | fftEy=fftEy[:,start:end], 58 | fftHx=fftHx[:,start:end], 59 | fftHy=fftHy[:,start:end], 60 | ux_list=ux_list, uy_list=uy_list_now, 61 | dxp=dxp, dyp=dyp, wavelength=wavelength, 62 | n_glass=n_glass) 63 | P_here_times_r2_over_uz[:, start:end] = temp 64 | 65 | start = end 66 | end = min(start+uy_pts_at_a_time, uy_list.size) 67 | 68 | P_here_times_r2_over_uz = fftshift(P_here_times_r2_over_uz) 69 | ux_list = fftshift(ux_list) 70 | uy_list = fftshift(uy_list) 71 | dux = ux_list[1] - ux_list[0] 72 | duy = uy_list[1] - uy_list[0] 73 | ux,uy = np.meshgrid(ux_list, uy_list, indexing='ij', sparse=True) 74 | total_P = (P_here_times_r2_over_uz * dux * duy)[np.isfinite(P_here_times_r2_over_uz)].sum() 75 | return P_here_times_r2_over_uz, total_P, ux, uy, dux, duy 76 | 77 | def farfield_from_nearfield_helper(fftEx, fftEy, fftHx, fftHy, ux_list, 78 | uy_list, dxp, dyp, wavelength, n_glass): 79 | """Don't run directly, this is called by farfield_from_nearfield(). 80 | For some entries of the FFT of Ex, Ey, Hx, Hy, with the corresponding 81 | direction cosines (in glass) ux_list, uy_list, calculate the outgoing power 82 | 83 | We analyze only a subset of the entries at a time so that we can do 84 | calculations without worrying that we'll run out of RAM. 85 | 86 | Note that fftEx[i,j] must correspond to the direction cosine 87 | (ux_list[i], uy_list[j]). But the lists are not sorted - we didn't run 88 | fftshift on them. 89 | """ 90 | assert fftEx.shape == fftEy.shape == fftHx.shape == fftHy.shape == (ux_list.size, uy_list.size) 91 | ux,uy = np.meshgrid(ux_list, uy_list, indexing='ij', sparse=True) 92 | 93 | """ 94 | (8.15): J=n×H, M=-n×E. n is the outward-pointing normal, i.e. +zhat. So 95 | Jy = Hx , Jx = -Hy , My = -Ex , Mx = Ey 96 | 97 | (8.17): N = integral J e^{-ikr' cos psi}, L = integral M e^{-ikr' cos psi} 98 | Note j=-i because I'm using e^+ikx but Taflove is using e^-jkx 99 | r'=(x',y',0) = position on the lens, psi = angle between r' and 100 | r = (ux,uy,uz) * infinity. We calculate r' cos psi = x'*ux + y'*uy, so 101 | now N = (integral over x',y') J e^{-i*k*(x'*ux + y'*uy)} 102 | Rewrite this as 103 | N(ux,uy) = Δx' * Δy' * (sum over x',y') J(x',y') e^{-i*k*(x'*ux + y'*uy)} 104 | Now I need to shift J... 105 | J[m1,m2] = J(x'min + m1 * Δx', y'min + m2 * Δy') 106 | Jshift[m1,m2] = J(m1 * Δx', m2 * Δy') 107 | This is what np.fft.fftshift() does.Of course this formula only really 108 | makes sense if J is periodic, which amounts to discretizing the frequency, 109 | but we were going to do that anyway. 110 | 111 | Now we have 112 | N(ux,uy) = Δx' * Δy' * (sum over m1,m2) Jshift[m1,m2] e^{-i*k*(m1 * Δx' * ux + m2 * Δy' * uy)} 113 | Next, let Jhat be the FFT of Jshift. The numpy conventions page says: 114 | http://docs.scipy.org/doc/numpy/reference/routines.fft.html 115 | Jhat[i1,i2] = (sum over m1,m2) J[m1,m2] e^{-2*pi*i*(i1*m1/num_x' + i2*m2/num_y')} 116 | I want this to match with . . . . . . . e^{-i*k*(m1 * Δx' * ux + m2 * Δy' * uy)} 117 | so we get the following relation between ux,uy and i1,i2: 118 | ux = (lambda/Δx') * i1 / num_x' , uy = (lambda/Δy') * i2 / num_y' 119 | With that, 120 | N(ux,uy) = Δx' * Δy' * Jhat[i1,i2] 121 | 122 | Note that the FFT won't distinguish between ux and (ux + lambda/Δx'). 123 | So if Δx' > lambda/2 then I cannot cover -1 < ux < +1 without 124 | aliasing different parts of that range together. This is what I expect. 125 | When you sample at exactly half the wavelength, you can't tell apart waves 126 | traveling at (+k,0,0) from (-k,0,0), i.e. perfectly glancing. 127 | 128 | From N = integral J e^{-ikr' cos psi} it seems intuitively clear that I 129 | should take refractive index into account by using k in the medium, i.e. 130 | replace wavelength with (wavelength/n_glass). I'm planning to allow some 131 | aliasing because I don't expect there to be much light with k_inplane > kvac 132 | because I'm excluding those modes. But I should double-check with proper sampling 133 | """ 134 | 135 | Nx = -fftHy * dxp * dyp 136 | Ny = fftHx * dxp * dyp 137 | Lx = fftEy * dxp * dyp 138 | Ly = -fftEx * dxp * dyp 139 | 140 | # uz = (1 - ux**2 - uy**2)**0.5 141 | # # note: uz is nan sometimes 142 | # Z = nu.Z0 / n_glass 143 | # Efar_sq_over_r2 = (abs(Ly * uz + Nx * Z)**2 + abs(-Lx * uz + Ny * Z)**2 144 | # + abs(Lx * uy - Ly * ux)**2) * (2*pi*n_glass/wavelength / (4*pi))**2 145 | # print('unittest', Efar_sq_over_r2[70,70] / ((nu.V/nu.m)**2 / nu.m**2)) 146 | # return 147 | 148 | """(8.23-4). Note that cos θ = uz, sin θ cos φ = ux, sin θ sin φ = uy 149 | Therefore cos θ cos φ = ux * sqrt(ux^2+uy^2) / uz 150 | ux / sin θ = ux / sqrt(1-uz^2)""" 151 | #uz = (1 - ux**2 - uy**2)**0.5 152 | # actually, I like avoiding the numpy RunTimeWarning 153 | uz = (1 - ux**2 - uy**2) 154 | uz[uz<0] = np.nan 155 | uz = uz**0.5 156 | sintheta = (ux**2 + uy**2)**0.5 157 | # add 1e-9 to avoid divide-by-zero RunTimeWarning, I'll fix it in a minute 158 | Ntheta = Nx * ux * uz / (sintheta + 1e-9) + Ny * uy * uz / (sintheta + 1e-9) 159 | Nphi = -Nx * uy / (sintheta + 1e-9) + Ny * ux / (sintheta + 1e-9) 160 | # for ux=uy=0, take the limit where uy=0, ux is positive infinitesimal 161 | i = np.where(ux==0)[0] 162 | j = np.where(uy==0)[0] 163 | Ntheta[i,j] = Nx[i,j] 164 | Nphi[i,j] = Ny[i,j] 165 | del Nx, Ny 166 | Ltheta = Lx * ux * uz / (sintheta + 1e-9) + Ly * uy * uz / (sintheta + 1e-9) 167 | Lphi = -Lx * uy / (sintheta + 1e-9) + Ly * ux / (sintheta + 1e-9) 168 | Ltheta[i,j] = Lx[i,j] 169 | Lphi[i,j] = Ly[i,j] 170 | del Lx, Ly, sintheta 171 | 172 | 173 | """(8.25)""" 174 | # We imagine a hemispherical surface of radius R, with local power P(ux,uy). 175 | # The total power flux is (integral over surface) P d(surface area) 176 | # with d(surface area) = (dux * duy / uz) * r^2. 177 | # Proof: Imagine you're looking overhead at a hemisphere. You draw a square 178 | #(from your POV). The area on the hemisphere that the square covers is 179 | # bigger than its projection in your POV, because of the slope. The ratio 180 | # is uz. 181 | # So all in all, (integral over surface) (P*r^2 / uz) dux duy 182 | 183 | Z = nu.Z0 / n_glass 184 | P_here_times_r2_over_uz = ((2*pi*n_glass/wavelength)**2 / (32*pi**2*Z) 185 | * (abs(Lphi + Z * Ntheta)**2 + abs(Ltheta - Z * Nphi)**2)) / (uz+1e-5) 186 | del uz 187 | 188 | # mystery factor - empty aperture should be 100% transmissive etc 189 | P_here_times_r2_over_uz *= 2 190 | 191 | return P_here_times_r2_over_uz 192 | 193 | 194 | ############# 195 | # This is an alternative implementation, following a different 196 | # reference. The results are exactly the same. This one is a bit slower though. 197 | 198 | #def farfield_from_nearfield2(Ex, Ey, Hx, Hy, xp_list, yp_list, wavelength, n_glass): 199 | # """following "Understanding the Finite-Difference Time-Domain Method", 200 | # John B. Schneider, www.eecs.wsu.edu/~schneidj/ufdtd 201 | # 202 | # xp, yp (really x', y', p stands for "prime") are coordinates on the lens, 203 | # with xp=yp=0 at the lens center 204 | # dxp (really Δx', p stands for "prime") is the spacing between sample points 205 | # on the lens 206 | # """ 207 | # dxp = xp_list[1] - xp_list[0] 208 | # dyp = yp_list[1] - yp_list[0] 209 | # num_x = len(xp_list) 210 | # num_y = len(yp_list) 211 | # assert Ex.shape == Ey.shape == Hx.shape == Hy.shape == (num_x, num_y) 212 | # for l in [xp_list,yp_list]: 213 | # diffs = [l[i+1] - l[i] for i in range(len(l)-1)] 214 | # assert 0 < diffs[0] < wavelength/2 215 | # assert max(diffs) - min(diffs) <= 1e-9 * max(abs(d) for d in diffs) 216 | # """ 217 | # (14.3-4): J=n×H, M=-n×E. n is the outward-pointing normal, i.e. +zhat 218 | # """ 219 | # Jy = Hx 220 | # Jx = -Hy 221 | # My = -Ex 222 | # Mx = Ey 223 | # """ 224 | # (8.17): N = integral J e^{-ikr' cos psi}, L = integral M e^{-ikr' cos psi} 225 | # Note j=-i because I'm using e^+ikx but Taflove is using e^-jkx 226 | # r'=(x',y',0) = position on the lens, psi = angle between r' and 227 | # r = (ux,uy,uz) * infinity. We calculate r' cos psi = x'*ux + y'*uy, so 228 | # now N = (integral over x',y') J e^{-i*k*(x'*ux + y'*uy)} 229 | # Rewrite this as 230 | # N(ux,uy) = Δx' * Δy' * (sum over x',y') J(x',y') e^{-i*k*(x'*ux + y'*uy)} 231 | # Now I need to shift J... 232 | # J[m1,m2] = J(x'min + m1 * Δx', y'min + m2 * Δy') 233 | # Jshift[m1,m2] = J(m1 * Δx', m2 * Δy') 234 | # This is what np.fft.fftshift() does.Of course this formula only really 235 | # makes sense if J is periodic, which amounts to discretizing the frequency, 236 | # but we were going to do that anyway. 237 | # 238 | # Now we have 239 | # N(ux,uy) = Δx' * Δy' * (sum over m1,m2) Jshift[m1,m2] e^{-i*k*(m1 * Δx' * ux + m2 * Δy' * uy)} 240 | # Next, let Jhat be the FFT of Jshift. The numpy conventions page says: 241 | # http://docs.scipy.org/doc/numpy/reference/routines.fft.html 242 | # Jhat[i1,i2] = (sum over m1,m2) J[m1,m2] e^{-2*pi*i*(i1*m1/num_x' + i2*m2/num_y')} 243 | # I want this to match with . . . . . . . e^{-i*k*(m1 * Δx' * ux + m2 * Δy' * uy)} 244 | # so we get the following relation between ux,uy and i1,i2: 245 | # ux = (lambda/Δx') * i1 / num_x' , uy = (lambda/Δy') * i2 / num_y' 246 | # With that, 247 | # N(ux,uy) = Δx' * Δy' * Jhat[i1,i2] 248 | # 249 | # Note that the FFT won't distinguish between ux and (ux + lambda/Δx'). 250 | # So if Δx' > lambda/2 then I cannot cover -1 < ux < +1 without 251 | # aliasing different parts of that range together. This is what I expect. 252 | # When you sample at exactly half the wavelength, you can't tell apart waves 253 | # traveling at (+k,0,0) from (-k,0,0), i.e. perfectly glancing. 254 | # 255 | # From N = integral J e^{-ikr' cos psi} it seems intuitively clear that I 256 | # should take refractive index into account by using k in the medium, i.e. 257 | # replace wavelength with (wavelength/n_glass). I'm planning to allow some 258 | # aliasing because I don't expect there to be much light with k_inplane > kvac 259 | # because I'm excluding those modes. But I should double-check with proper sampling 260 | # """ 261 | # ux,uy = np.meshgrid(np.arange(num_x) * (wavelength/n_glass) / (dxp * num_x), 262 | # np.arange(num_y) * (wavelength/n_glass) / (dyp * num_y), indexing='ij') 263 | # # little test 264 | # if True: 265 | # i1,i2 = 1,3 266 | # assert ux[i1,i2] == (wavelength/n_glass) * i1 / (dxp * num_x) 267 | # assert uy[i1,i2] == (wavelength/n_glass) * i2 / (dyp * num_y) 268 | # ux[ux > ux.max()/2] -= (wavelength/n_glass) / dxp 269 | # uy[uy > uy.max()/2] -= (wavelength/n_glass) / dyp 270 | # 271 | # Nx = fft2(fftshift(Jx)) * dxp * dyp 272 | # Ny = fft2(fftshift(Jy)) * dxp * dyp 273 | # Lx = fft2(fftshift(Mx)) * dxp * dyp 274 | # Ly = fft2(fftshift(My)) * dxp * dyp 275 | # 276 | # # We have Nx[i,j] corresponding to the direction (ux[i,j], uy[i,j]). That's 277 | # # fine. But it's nice to have everything sorted, for imshow plots etc. 278 | # ux = fftshift(ux) 279 | # uy = fftshift(uy) 280 | # Nx = fftshift(Nx) 281 | # Ny = fftshift(Ny) 282 | # Lx = fftshift(Lx) 283 | # Ly = fftshift(Ly) 284 | # 285 | # """ 286 | # Above we said 287 | # 288 | # N = integral J e^{-ikr' cos psi}, L = integral M e^{-ikr' cos psi} 289 | # 290 | # Compare to (14.32) (remember that we are switching the sign of i to use 291 | # e^+ikr, but also e^-ik|r-r'| turns into e^+ikr'sin(psi). So the sign is 292 | # right). 293 | # We get A(r) = (mu0/4pi) N e^ikr/r 294 | # F(r) = (eps/4pi) L e^ikr/r 295 | # 296 | # Then (14.52) says 297 | # E = +iw [A - khat(khat dot A)] - 1j/eps * k cross F 298 | # """ 299 | # uz = (1 - ux**2 - uy**2)**0.5 300 | # eps = nu.eps0 * n_glass**2 301 | # Ax_over_eikrr = Nx * nu.mu0 / (4*pi) 302 | # Ay_over_eikrr = Ny * nu.mu0 / (4*pi) 303 | # Fx_over_eikrr = Lx * eps / (4*pi) 304 | # Fy_over_eikrr = Ly * eps / (4*pi) 305 | # u_dot_A_over_eikrr = Ax_over_eikrr * ux + Ay_over_eikrr * uy 306 | # kx = (2*pi*n_glass / wavelength) * ux 307 | # ky = (2*pi*n_glass / wavelength) * uy 308 | # kz = (2*pi*n_glass / wavelength) * uz 309 | # 310 | # omega = nu.c0 * 2*pi/wavelength 311 | # Ex_over_eikrr = (1j * omega * (Ax_over_eikrr - ux * u_dot_A_over_eikrr) 312 | # - 1j/eps * (-kz * Fy_over_eikrr)) 313 | # Ey_over_eikrr = (1j * omega * (Ay_over_eikrr - uy * u_dot_A_over_eikrr) 314 | # - 1j/eps * (kz * Fx_over_eikrr)) 315 | # Ez_over_eikrr = (1j * omega * ( 0 - uz * u_dot_A_over_eikrr) 316 | # - 1j/eps * (kx * Fy_over_eikrr - ky * Fx_over_eikrr)) 317 | # absE2_times_r2 = abs(Ex_over_eikrr)**2 + abs(Ey_over_eikrr)**2 + abs(Ez_over_eikrr)**2 318 | # 319 | # #print('unittest', absE2_over_r2[70,70] / ((nu.V/nu.m)**2 * nu.m**2)) 320 | # 321 | # Z = nu.Z0 / n_glass 322 | # 323 | # #Power flow per area in the direction normal to the direction it's flowing 324 | # P_here_times_r2 = absE2_times_r2 / Z 325 | # 326 | # # We imagine a hemispherical surface of radius R, with local power P(ux,uy). 327 | # # The total power flux is (integral over surface) P dsurface 328 | # # d(surface area) = (dux * duy / uz) * r^2. 329 | # # So all in all, (integral over surface) (P*r^2 / uz) dux duy 330 | # 331 | # dux = ux[1,0] - ux[0,0] 332 | # duy = uy[0,1] - uy[0,0] 333 | # P_here_times_r2_over_uz = P_here_times_r2 / uz 334 | # #P_here_times_r2_over_uz[1-np.isfinite(P_here_times_r2_over_uz)] = 0 335 | # #P_here_times_r2_over_uz[np.isnan(P_here_times_r2_over_uz)] = 0 336 | # total_P = (P_here_times_r2_over_uz * dux * duy)[np.isfinite(P_here_times_r2_over_uz)].sum() 337 | # return P_here_times_r2_over_uz, total_P, ux, uy, dux, duy 338 | # 339 | # 340 | -------------------------------------------------------------------------------- /design_collimator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | (C) 2015 Steven Byrnes 4 | 5 | After you create the pieces for a meta-lens (GratingCollection's from 6 | grating.py for the lens periphery, and HexGridSet's from lens_center.py for the 7 | lens center), the functions here glue them together into a lens, and export 8 | the final design in summarized form, or as an explicit list of nano-pillars, 9 | or as a DXF or SVG file. 10 | """ 11 | 12 | from __future__ import division, print_function 13 | import math 14 | from math import pi 15 | degree = pi / 180 16 | import numpy as np 17 | import matplotlib.pyplot as plt 18 | from numericalunits import um, nm 19 | # http://pythonhosted.org/dxfwrite/ 20 | from dxfwrite import DXFEngine as dxf 21 | #https://pypi.python.org/pypi/ezdxf/ 22 | import ezdxf 23 | # http://svgwrite.readthedocs.org/en/latest/ 24 | import svgwrite 25 | 26 | import os 27 | inf = float('inf') 28 | 29 | #import loop_post_analysis 30 | import grating 31 | import lens_center 32 | 33 | # pitch is cylinder center-to-center separation, which is also the lateral period 34 | pitch = 320*nm 35 | period = pitch * math.sqrt(3) # in meters 36 | cyl_height = 550*nm 37 | n_glass = 0 38 | n_tio2 = 0 39 | 40 | FOLDER_NAME = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'temp') 41 | 42 | xyrra_filename = os.path.join(FOLDER_NAME, 'collimator_xyrra_list.txt') 43 | setup_filename = os.path.join(FOLDER_NAME, 'collimator_setup.txt') 44 | 45 | ############################################################################# 46 | 47 | # radius of collimator 48 | #radius = 410*um 49 | 50 | #source_distance = 150*um 51 | wavelength = 580*nm # in vacuum 52 | # refractive index in the medium filling the space between the source and the lens 53 | #refractive_index = 1.458 54 | refractive_index = 1 55 | refractive_index_in_glass = n_glass 56 | 57 | def target_phase(x, source_distance): 58 | """ x is the distance away from the center of the lens """ 59 | k = 2 * pi * refractive_index / wavelength 60 | return (-k * (math.sqrt(source_distance**2 + x**2) - source_distance)) % (2*pi) 61 | 62 | def target_phase_zeros(radius, source_distance): 63 | ans = [] 64 | order=0 65 | k = 2 * pi * refractive_index / wavelength 66 | while len(ans)==0 or ans[-1] < radius: 67 | x = (((2*pi*order)/k + source_distance)**2 - source_distance**2)**0.5 68 | ans.append(x) 69 | order += 1 70 | return ans 71 | 72 | 73 | 74 | def hexagonal_grid(n, radius, fourfold_symmetry=True): 75 | """calculate a list of (x,y) coordinates on a hexagonal grid with 76 | given nearest-neighbor separation n, only with x^2 + y^2 < radius^2. 77 | If fourfold_symmetry is True, restrict the design to x,y >= 0, with 78 | mirror-flips to get to the other quadrants. (Note that x=0 or y=0 points 79 | will get duplicated.)""" 80 | # basis vectors are 81 | # v1 = (0,neighbor_separation) 82 | # v2 = (neighbor_separation*sqrt(3)/2 , neighbor_separation/2) 83 | # if (x,y) = n1*v1 + n2*v2, then 84 | # y = n*(n1 + n2/2) ; x = n*n2*sqrt(3)/2 85 | # n1 = y/n - n2/2 ; n2 = 2x/(n*sqrt(3)) 86 | 87 | # calculate (n1,n2) at a few points to figure out bounds on how big they 88 | # could possibly be (we'll still test the points individually within that 89 | # range) 90 | if fourfold_symmetry is True: 91 | xy_corner_list = [(0,0), (radius,0), (0,radius), (radius,radius)] 92 | else: 93 | xy_corner_list = [(radius,radius), (radius, -radius), (-radius, radius), (-radius, -radius)] 94 | n1n2_corner_list = [(y/n - x/(n * 3**0.5), 2*x/(n * 3**0.5)) for x,y in xy_corner_list] 95 | xy_list = [] 96 | min_n1 = int(min(n1 for n1,n2 in n1n2_corner_list)) - 2 97 | max_n1 = int(max(n1 for n1,n2 in n1n2_corner_list)) + 2 98 | min_n2 = int(min(n2 for n1,n2 in n1n2_corner_list)) - 2 99 | max_n2 = int(max(n2 for n1,n2 in n1n2_corner_list)) + 2 100 | 101 | for n2 in range(min_n2, max_n2+1): 102 | x = n * n2 * 3**0.5/2 103 | for n1 in range(min_n1, max_n1+1): 104 | y = n * (n1+n2/2) 105 | if fourfold_symmetry is True: 106 | if x >= 0 and y >= 0 and x**2 + y**2 < radius**2: 107 | xy_list.append([x,y]) 108 | else: 109 | if x**2 + y**2 < radius**2: 110 | xy_list.append([x,y]) 111 | if False: 112 | #display the points 113 | plt.figure() 114 | for x,y in xy_list: 115 | plt.plot(x,y, 'k.') 116 | plt.gca().set_aspect('equal') 117 | 118 | return np.array(xy_list) 119 | 120 | def design_center(hgs, source_distance, radius): 121 | """design a metasurface lens following the "traditional" approach of laying 122 | out cylinders with centers on a hexagonal grid. hgs is a HexGridSet. 123 | 124 | Return a lens_center_summary [[xcenter, ycenter, index in hgs] in arbitrary 125 | order""" 126 | assert isinstance(hgs, lens_center.HexGridSet) 127 | xy_list = hexagonal_grid(pitch, radius, fourfold_symmetry=False) 128 | xyi_list = [] 129 | for x,y in xy_list: 130 | target_phase_here = target_phase((x**2+y**2)**0.5, source_distance) 131 | # TODO 132 | # adding pi seems necessary to make the grating part and center part 133 | # add in phase (based on inspecting the design, I haven't worked 134 | # through all the conventions used) 135 | target_phase_here += pi 136 | xyi_list.append([x,y,hgs.pick_from_phase(target_phase_here)]) 137 | return np.array(xyi_list) 138 | 139 | def make_center_xyrra_list(hgs, lens_center_summary): 140 | """HexGridSet, lens_center_summary""" 141 | assert isinstance(hgs, lens_center.HexGridSet) 142 | xyrra_list = [] 143 | for x,y,i in lens_center_summary: 144 | r = hgs.grating_list[int(i)].xyrra_list[0,2] 145 | xyrra_list.append([x,y,r,r,0]) 146 | return np.array(xyrra_list) 147 | 148 | def design_periphery(collections, source_distance, radius): 149 | """collections is a list 150 | [[(phi_start0,phi_end0), gratingcollection0], 151 | [(phi_start1,phi_end1), gratingcollection1], 152 | ...] 153 | where phi_end0 == phi_start1, etc. 154 | Returns lens_periphery_summary, a dictionary containing: 155 | * 'r_center_list' (np.array) the radius at the center of each 156 | subsequent grating 157 | * 'r_min_list' (np.array) the radius at the inner boundary of this 158 | grating 159 | * 'grating_period_list' (np.array), the period of the corresponding 160 | grating. Note that 161 | r_center_list[i] + 0.5 * grating_period_list[i] 162 | + 0.5 * grating_period_list[i+1] == r_center_list[i+1] 163 | * 'gratingcollection_list' (list), GratingCollection objects from 164 | inside to out (list will look like [gc0, gc1, gc2, ...]) 165 | * 'gratingcollection_index_here_list' (np.array), the index of the 166 | applicable gratingcollection object for each ring of gratings 167 | (indexed from the list above). Looks like [0,0,...,1,1,...2,2...]. 168 | * 'num_around_circle_list' (np.array), how many copies are there 169 | around 2pi, for each entry of the above. Looks like 170 | [n1,n1 ...,n2,n2,...]. 171 | """ 172 | for i in range(len(collections)-1): 173 | assert collections[i][0][1] == collections[i+1][0][0] 174 | assert all(x[0][0] < x[0][1] for x in collections) 175 | assert len(collections) > 0 176 | def num_around_circle(gc): 177 | """The patterns go in a wedge of angle 2*pi/num_around_circle 178 | radians. This calculates num_around_circle for a given 179 | GratingCollection gc. 180 | 181 | For round lenses, the gratingcollection.lateral_period parameter is 182 | redefined as 183 | "lateral_period at a certain point / tan(angle_in_air) at that point" 184 | Turns out: 2*pi*source_distance / (lateral period at x / tan(angle_in_air at x)) 185 | == (2*pi*x / lateral_period) == num_around_circle""" 186 | return int(round(2*pi*source_distance / gc.lateral_period)) 187 | 188 | r_center_list = [] 189 | grating_period_list = [] 190 | gratingcollection_index_here_list = [] 191 | num_around_circle_list = [] 192 | collection_index = 0 193 | angle_for_switch = collections[0][0][0] 194 | phase_zeros = [x for x in target_phase_zeros(radius+2*um, source_distance) if x > source_distance * math.tan(angle_for_switch)] 195 | if len(phase_zeros) <= 1: 196 | raise ValueError('Periphery is too small for even one ring') 197 | phase_zero_index = 0 198 | 199 | while True: 200 | r_outer = phase_zeros[phase_zero_index+1] 201 | r_inner = phase_zeros[phase_zero_index] 202 | r_center = (r_outer + r_inner) / 2 203 | angle_in_air = math.atan(r_center / source_distance) 204 | if collections[collection_index][0][1] < angle_in_air: 205 | collection_index += 1 206 | if collection_index >= len(collections): 207 | raise ValueError('radius is too big for provided collections') 208 | continue 209 | collection = collections[collection_index][1] 210 | assert isinstance(collection, grating.GratingCollection) 211 | num_around_circle_here = num_around_circle(collection) 212 | num_around_circle_list.append(num_around_circle_here) 213 | r_center_list.append(r_center) 214 | grating_period_list.append(r_outer-r_inner) 215 | gratingcollection_index_here_list.append(collection_index) 216 | if r_outer > radius: 217 | break 218 | phase_zero_index += 1 219 | r_center_list = np.array(r_center_list) 220 | grating_period_list = np.array(grating_period_list) 221 | lens_periphery_summary = {'gratingcollection_list': [i[1] for i in collections], 222 | 'r_center_list': r_center_list, 223 | 'r_min_list' : r_center_list - 0.5 * grating_period_list, 224 | 'r_max_list' : r_center_list + 0.5 * grating_period_list, 225 | 'grating_period_list': grating_period_list, 226 | 'gratingcollection_index_here_list': np.array(gratingcollection_index_here_list), 227 | 'num_around_circle_list': np.array(num_around_circle_list)} 228 | return lens_periphery_summary 229 | 230 | def make_periphery_xyrra_list(lens_periphery_summary): 231 | """lens_periphery_summary is as outputted by design_periphery()""" 232 | num_around_circle_list = lens_periphery_summary['num_around_circle_list'] 233 | gratingcollection_list = lens_periphery_summary['gratingcollection_list'] 234 | gratingcollection_index_here_list = lens_periphery_summary['gratingcollection_index_here_list'] 235 | grating_period_list = lens_periphery_summary['grating_period_list'] 236 | r_center_list = lens_periphery_summary['r_center_list'] 237 | xyrra_list = [] 238 | num_rings = len(num_around_circle_list) 239 | for i in range(num_rings): 240 | num_around_circle_here = num_around_circle_list[i] 241 | gc_here = gratingcollection_list[gratingcollection_index_here_list[i]] 242 | assert isinstance(gc_here, grating.GratingCollection) 243 | grating_period = grating_period_list[i] 244 | xyrra_list_here = gc_here.get_one(grating_period=grating_period).xyrra_list 245 | if i != 0 and gratingcollection_index_here_list[i] == gratingcollection_index_here_list[i-1]: 246 | # check for pillars passing through the periodic boundary 247 | # TODO - test this routine 248 | xyrra_list_prev = gc_here.get_one(grating_period=grating_period_list[i-1]).xyrra_list 249 | assert xyrra_list_prev.shape == xyrra_list_here.shape 250 | for j in range(xyrra_list_here.shape[0]): 251 | if (xyrra_list_prev[j,0] > 0.8 * grating_period 252 | and xyrra_list_here[j,0] < 0.2 * grating_period): 253 | # want to avoid drawing this ellipse twice 254 | xyrra_list_here = np.delete(xyrra_list_here, j, axis=0) 255 | # break because the lists are not aligned anymore and I 256 | # doubt this will happen with two ellipses simultaneously 257 | break 258 | if (xyrra_list_prev[j,0] < 0.2 * grating_period 259 | and xyrra_list_here[j,0] > 0.8 * grating_period): 260 | # want to avoid omitting this ellipse altogether 261 | xyrra_list_here = np.vstack((xyrra_list_here, [xyrra_list_prev[j,:]])) 262 | # break because the lists are not aligned anymore and I 263 | # doubt this will happen with two ellipses simultaneously 264 | break 265 | for angle in np.linspace(0, 2*pi, num=num_around_circle_here, endpoint=False): 266 | for x,y,rx,ry,a in xyrra_list_here: 267 | x += r_center_list[i] 268 | xyrra_list.append([x * math.cos(angle) - y * math.sin(angle), 269 | x * math.sin(angle) + y * math.cos(angle), 270 | rx, ry, angle+a]) 271 | return np.array(xyrra_list) 272 | 273 | def make_design(collections, source_distance, radius, hgs, make_xyrra_list=False): 274 | """calculate the design for a full round lens, including both the 275 | center and the periphery. hgs is the HexGridSet used for the center (see 276 | lens_center.py). 277 | collections is of the form [[(15*degree, 20*degree), my_grating_collection_A], 278 | [(20*degree, 33.2*degree), my_grating_collection_B], 279 | [(33.2*degree, 45*degree), my_grating_collection_C], 280 | ...] 281 | If make_xyrra_list is True, also return the full xyrra_list 282 | specifying the center (x,y), radii (rx,ry), and rotation angle a for every 283 | nano-pillar in the whole lens -- input for make_dxf() etc.""" 284 | if len(collections) > 0: 285 | n_tio2 = hgs.n_tio2 286 | n_glass = hgs.n_glass 287 | cyl_height = hgs.cyl_height 288 | for _,gc in collections: 289 | assert gc.lens_type == 'round' 290 | for g in gc.grating_list: 291 | assert g.n_tio2 == n_tio2 292 | assert g.n_glass == n_glass 293 | assert g.cyl_height == cyl_height 294 | lens_periphery_summary = design_periphery(collections, source_distance, radius) 295 | if make_xyrra_list: 296 | periphery_xyrra_list = make_periphery_xyrra_list(lens_periphery_summary) 297 | r_for_switch = lens_periphery_summary['r_min_list'][0] 298 | assert r_for_switch < radius 299 | else: 300 | r_for_switch = radius 301 | periphery_xyrra_list = None 302 | lens_periphery_summary = None 303 | 304 | lens_center_summary=design_center(hgs, source_distance, r_for_switch-300*nm) 305 | 306 | if make_xyrra_list: 307 | center_xyrra_list = make_center_xyrra_list(hgs, lens_center_summary) 308 | if periphery_xyrra_list is not None: 309 | xyrra_list = np.vstack((center_xyrra_list, periphery_xyrra_list)) 310 | else: 311 | xyrra_list = center_xyrra_list 312 | return lens_periphery_summary, lens_center_summary, r_for_switch, xyrra_list 313 | return lens_periphery_summary, lens_center_summary, r_for_switch 314 | 315 | 316 | 317 | def make_dxf(xyrra_list): 318 | """turn an xyrra_list (xcenter, ycenter, radius_x, radius_y, angle of rotation) 319 | into a dxf file """ 320 | directory_now = os.path.dirname(os.path.realpath(__file__)) 321 | drawing = dxf.drawing(os.path.join(directory_now, 'test.dxf')) 322 | for i in range(xyrra_list.shape[0]): 323 | if i % 10000 == 0: 324 | print(xyrra_list.shape[0] - i, 'ellipses remaining in dxf creation...', flush=True) 325 | x,y,rx,ry,a = xyrra_list[i,:] 326 | if rx == ry: 327 | circ = dxf.circle(radius=rx / um, center=(x/um,y/um)) 328 | drawing.add(circ) 329 | else: 330 | ellipse = dxf.ellipse((x/um,y/um), rx/um, ry/um, rotation=a/degree, segments=16) 331 | drawing.add(ellipse) 332 | print('saving dxf...', flush=True) 333 | drawing.save() 334 | 335 | def make_dxf2(xyrra_list): 336 | """turn an xyrra_list (xcenter, ycenter, radius_x, radius_y, angle of rotation) 337 | into a dxf file. Should be identical to make_dxf(). Seems to run 3X faster. 338 | 339 | Note: When I tried this, it looked good except for 2 random polylines near 340 | the center that I had to delete by hand. Weird! Always check your files.""" 341 | directory_now = os.path.dirname(os.path.realpath(__file__)) 342 | 343 | dwg = ezdxf.new('AC1015') 344 | msp = dwg.modelspace() 345 | 346 | points = [(0, 0), (3, 0), (6, 3), (6, 6)] 347 | msp.add_lwpolyline(points) 348 | 349 | for i in range(xyrra_list.shape[0]): 350 | if i % 10000 == 0: 351 | print(xyrra_list.shape[0] - i, 'ellipses remaining in dxf creation...', flush=True) 352 | x,y,rx,ry,a = xyrra_list[i,:] 353 | if rx == ry: 354 | msp.add_circle((x/um,y/um), rx / um) 355 | else: 356 | ellipse_pts = grating.ellipse_pts(x/um,y/um, rx/um, ry/um, a, num_points=16) 357 | # repeat first point twice 358 | ellipse_pts = np.vstack((ellipse_pts, ellipse_pts[0,:])) 359 | msp.add_lwpolyline(ellipse_pts) 360 | print('saving dxf...', flush=True) 361 | dwg.saveas(os.path.join(directory_now, "test2.dxf")) 362 | 363 | def make_svg(xyrra_list): 364 | """like make_dxf but it's an svg file instead. This is twice as slow as 365 | making the dxf, but 10X smaller file size, and easy fast viewing with 366 | Inkscape or other programs.""" 367 | directory_now = os.path.dirname(os.path.realpath(__file__)) 368 | dwg = svgwrite.Drawing(filename=os.path.join(directory_now, 'test.svg')) 369 | for i in range(xyrra_list.shape[0]): 370 | if i % 10000 == 0: 371 | print(xyrra_list.shape[0] - i, 'ellipses remaining in svg creation...') 372 | x,y,rx,ry,a = xyrra_list[i,:] 373 | if rx == ry: 374 | circ = svgwrite.shapes.Circle(center=(x/um,y/um), r=rx / um) 375 | dwg.add(circ) 376 | else: 377 | ellipse = svgwrite.shapes.Ellipse(center=(x/um,y/um), r=(rx/um, ry/um)) 378 | ellipse.rotate(angle=a/degree, center=(x/um,y/um)) 379 | dwg.add(ellipse) 380 | print('saving svg...', flush=True) 381 | dwg.save() 382 | 383 | -------------------------------------------------------------------------------- /grating.lua: -------------------------------------------------------------------------------- 1 | --(C) 2015 Steven Byrnes 2 | 3 | --This is the lua script that S4 runs. Generally you don't run this yourself, you let Python call it 4 | --(see grating.py) 5 | 6 | pi = math.pi 7 | degree = pi / 180 8 | math.randomseed(os.time()) 9 | 10 | function almost_equal(a,b,tol) 11 | return math.abs(a-b) <= tol * (math.abs(a) + math.abs(b)) 12 | end 13 | 14 | function str_from_complex(a) 15 | return string.format('%.4f + %.4f i', a[1], a[2]) 16 | end 17 | 18 | function polar_str_from_complex(a) 19 | local phase = math.atan2(a[1], a[2]) 20 | if phase < 0 then 21 | phase = phase + 2*pi 22 | end 23 | return string.format('amp:%.4f, phase:%.4fdeg', math.sqrt(a[1]^2 + a[2]^2), phase/deg) 24 | end 25 | 26 | function mergeTables(t1, t2) 27 | --for hash tables. With duplicate entries, t2 will overwrite. 28 | local out = {} 29 | for k,v in pairs(t1) do out[k] = v end 30 | for k,v in pairs(t2) do out[k] = v end 31 | return out 32 | end 33 | 34 | -- read parameters from configuration files (hopefully just now outputted by Python) 35 | -- all lengths in microns 36 | f = assert(io.open("temp/grating_setup.txt", "r")) 37 | what_to_do = f:read("*line") 38 | assert(what_to_do == '1' or what_to_do == '2') 39 | if what_to_do == '1' then what_to_do = 'fom' end 40 | if what_to_do == '2' then what_to_do = 'characterize' end 41 | 42 | if what_to_do == 'fom' then 43 | grating_period = tonumber(f:read("*line")) * 1e6 44 | lateral_period = tonumber(f:read("*line")) * 1e6 45 | angle_in_air = tonumber(f:read("*line")) 46 | n_glass = tonumber(f:read("*line")) 47 | nTiO2 = tonumber(f:read("*line")) 48 | cyl_height = tonumber(f:read("*line")) * 1e6 49 | --num_G is the number of modes to include. Higher = slower but more accurate 50 | num_G = tonumber(f:read("*line")) 51 | elseif what_to_do == 'characterize' then 52 | grating_period = tonumber(f:read("*line")) * 1e6 53 | lateral_period = tonumber(f:read("*line")) * 1e6 54 | n_glass = tonumber(f:read("*line")) 55 | nTiO2 = tonumber(f:read("*line")) 56 | cyl_height = tonumber(f:read("*line")) * 1e6 57 | num_G = tonumber(f:read("*line")) 58 | --ux,uy is direction cosine 59 | ux_min = tonumber(f:read("*line")) 60 | ux_max = tonumber(f:read("*line")) 61 | uy_min = tonumber(f:read("*line")) 62 | uy_max = tonumber(f:read("*line")) 63 | u_steps = tonumber(f:read("*line")) 64 | wavelength = tonumber(f:read("*line")) 65 | end 66 | 67 | 68 | --setting either nTiO2 or n_glass to 0 means "use tabulated data". 69 | -- this table is generated by refractive_index.py 70 | if nTiO2 == 0 then 71 | nTiO2_data = 72 | {[450]=2.5, 73 | [500]=2.433, 74 | [525]=2.41, 75 | [550]=2.391, 76 | [575]=2.375, 77 | [580]=2.372, 78 | [600]=2.362, 79 | [625]=2.351, 80 | [650]=2.341} 81 | end 82 | if n_glass == 0 then 83 | nSiO2_data = 84 | {[450]=1.466, 85 | [500]=1.462, 86 | [525]=1.461, 87 | [550]=1.46, 88 | [575]=1.459, 89 | [580]=1.459, 90 | [600]=1.458, 91 | [625]=1.457, 92 | [650]=1.457} 93 | end 94 | 95 | f = assert(io.open("temp/grating_xyrra_list.txt", "r")) 96 | 97 | xyrra_list = {} 98 | while f:read(0) == '' do 99 | local line = f:read("*line") 100 | --print('line' .. line) 101 | local t={} 102 | local i=1 103 | for str in string.gmatch(line, "([^%s]+)") do 104 | t[i] = tonumber(str) 105 | i = i + 1 106 | end 107 | if #t > 0 then xyrra_list[#xyrra_list+1] = t end 108 | end 109 | 110 | function set_up() 111 | --initial setup of a simulation 112 | 113 | local S = S4.NewSimulation() 114 | S:SetLattice({grating_period,0}, {0, lateral_period}) 115 | S:SetNumG(num_G) 116 | ------- Materials ------- 117 | --S:AddMaterial(name, {real part of epsilon, imag part of epsilon}) 118 | --if nTiO2 or n_glass is 0, don't worry, we'll set it later in 119 | --set_wavelength_angle() when we know the wavelength. 120 | S:AddMaterial("Air", {1,0}) 121 | S:AddMaterial("TiO2", {nTiO2^2,0}) 122 | S:AddMaterial("Glass", {n_glass^2,0}) 123 | 124 | ------- Layers ------- 125 | -- S:AddLayer(name, thickness, default material) 126 | S:AddLayer('Air', 0, 'Air') 127 | S:AddLayer('Cylinders', cyl_height, 'Air') 128 | S:AddLayer('Substrate', 0, 'Glass') 129 | 130 | for _,xyrra in ipairs(xyrra_list) do 131 | x,y,rx,ry,angle = unpack(xyrra) 132 | --print(x,y,rx,ry,angle) 133 | --S:SetLayerPatternEllipse(layer, material, center, angle (CCW, in degrees), halfwidths) 134 | S:SetLayerPatternEllipse('Cylinders', 'TiO2', {x,y}, angle, {rx,ry}) 135 | end 136 | return S 137 | end 138 | 139 | function set_wavelength_angle(arg) 140 | --set the wavelength and angle of incoming light 141 | --S is the result of set_up() 142 | local S = arg.S 143 | local pol = arg.pol 144 | assert((pol == 's') or (pol == 'p')) 145 | local wavelength = arg.wavelength 146 | local incident_theta = arg.incident_theta 147 | local incident_phi = arg.incident_phi 148 | 149 | if nTiO2 == 0 then 150 | local nTiO2_now = nTiO2_data[math.floor(wavelength*1000+0.5)] 151 | assert(nTiO2_now > 0) 152 | S:SetMaterial("TiO2", {nTiO2_now^2,0}) 153 | end 154 | local n_glass_now 155 | if n_glass == 0 then 156 | n_glass_now = nSiO2_data[math.floor(wavelength*1000+0.5)] 157 | assert(n_glass_now > 0) 158 | S:SetMaterial("Glass", {n_glass_now^2,0}) 159 | else 160 | n_glass_now = n_glass 161 | end 162 | 163 | local sAmplitude, pAmplitude 164 | if pol == 's' then 165 | sAmplitude = {1,0} -- amplitude and phase 166 | pAmplitude = {0,0} 167 | else 168 | sAmplitude = {0,0} 169 | pAmplitude = {1,0} 170 | end 171 | 172 | S:SetFrequency(1/wavelength) 173 | -- we set it up so that the incoming wave is the (0,0) diffraction order 174 | S:SetExcitationPlanewave( 175 | {incident_theta/degree,incident_phi/degree}, -- incidence angles (spherical coordinates: phi in [0,180], theta in [0,360]) 176 | sAmplitude, -- s-polarization amplitude and phase (in degrees) 177 | pAmplitude) -- p-polarization amplitude and phase 178 | 179 | --futz with these settings to make it more accurate for a given num_G 180 | S:UsePolarizationDecomposition() 181 | S:UseNormalVectorBasis() 182 | --S:UseJonesVectorBasis() 183 | --S:UseSubpixelSmoothing() 184 | --S:SetResolution(4) 185 | return {S, n_glass_now} 186 | end 187 | 188 | function fom(arg) 189 | -- Calculate a figure-of-merit, basically the amount of power going into the desired diffraction order. 190 | -- This is used for optimizing the gratings. 191 | 192 | --S is output from set_up() 193 | local S = arg.S 194 | local target_diffraction_order = arg.target_diffraction_order 195 | local incident_theta = arg.incident_theta 196 | local pol = arg.pol 197 | local wavelength = arg.wavelength 198 | local inphase = arg.inphase 199 | 200 | local S, n_glass_now = unpack(set_wavelength_angle{pol=pol, S=S, wavelength=wavelength, 201 | incident_theta=incident_theta, incident_phi=0}) 202 | 203 | local outgoing_order_index = S:GetDiffractionOrder(target_diffraction_order,0) 204 | local forw,_ = S:GetAmplitudes('Substrate',0) 205 | local complex_output_amplitude 206 | if pol == 's' then 207 | complex_output_amplitude = forw[outgoing_order_index] 208 | --at normal incidence (i.e. center of lens), it seems that s and p wind up with opposite phases due to some weird convention 209 | --(I think ... I still need to check explicitly). To keep 210 | --everything adding in phase, we need to keep the same s-vs-p phase relation throughout the lens 211 | complex_output_amplitude = {-complex_output_amplitude[1], -complex_output_amplitude[2]} 212 | else 213 | complex_output_amplitude = forw[outgoing_order_index + S:GetNumG()] 214 | end 215 | -- End result: Power in desired diffraction order 216 | if inphase then 217 | return math.abs(complex_output_amplitude[2]) * complex_output_amplitude[2] / n_glass_now / math.cos(incident_theta) 218 | else 219 | return (complex_output_amplitude[1]^2 + complex_output_amplitude[2]^2) / n_glass_now / math.cos(incident_theta) 220 | end 221 | -- We are looking at the imaginary part complex_output_amplitude[2], rather than the absolute 222 | -- value complex_output_amplitude[1]^2 + complex_output_amplitude[2]^2, because we want the 223 | -- output phases of different parts of the lens to agree. 224 | -- We are looking at the imaginary part complex_output_amplitude[2], rather than the real 225 | -- part complex_output_amplitude[1], for no particular reason, they would each work equally 226 | -- well as an optimization target. 227 | -- We are looking at math.abs(complex_output_amplitude[2]) * complex_output_amplitude[2], rather 228 | -- than complex_output_amplitude[2]^2, because we want to have consistent phase, no possible 229 | -- sign-flip. 230 | -- We are using S:GetAmplitudes() rather than S:GetPowerFluxByOrder() because the latter 231 | -- is calculated in the region where the different modes are all overlapping. Also, we want 232 | -- just the s --> s and p --> p polarization-conserved power because it's easier to think 233 | -- about that than the phases of two different polarizations at once. That means it's 234 | -- perhaps an underestimate of collimated power. 235 | 236 | --consistency check: The following two should match (at least approximately 237 | --return (complex_output_amplitude[1]^2 + complex_output_amplitude[2]^2) / n_glass_now / math.cos(incident_theta) 238 | -- ..... VS ..... 239 | --[[ 240 | local incident_power, total_reflected_power = S:GetPowerFlux('Air', 0) 241 | local total_transmitted_power, temp = S:GetPowerFlux('Substrate', 0) 242 | assert(temp==0) 243 | local transmitted_normal_power, back_r, forw_i, back_i = unpack(S:GetPowerFluxByOrder('Substrate',0)[outgoing_order_index]) 244 | assert(almost_equal(-total_reflected_power + total_transmitted_power, incident_power, 1e-3)) 245 | assert(back_i == 0 and back_r == 0 and forw_i == 0) 246 | return (transmitted_normal_power / incident_power) 247 | --]] 248 | 249 | -- This is the formula I was using earlier 250 | --local phase = math.atan2(complex_output_amplitude[1], complex_output_amplitude[2]) 251 | -- note: I got the atan2 arguments backwards, so that's really 90 degrees minus the phase. Doesn't matter. 252 | --return (transmitted_normal_power / incident_power) * math.cos(phase) 253 | end 254 | 255 | function print_orders(arg) 256 | -- This function will print the complex amplitude for the requested diffraction 257 | -- orders. This is used for far-field calculations. 258 | 259 | local S = arg.S 260 | local pol = arg.pol 261 | assert((pol == 's') or (pol == 'p')) 262 | local wavelength = arg.wavelength 263 | -- orders should be an array like {{0,0},{1,0},...} -- diffraction orders to calculate 264 | local orders=arg.orders 265 | assert(orders ~= nil) 266 | --ux,uy are direction cosines. They are outputted to make the results easier to parse, 267 | -- but otherwise are not used 268 | local ux = arg.ux 269 | local uy = arg.uy 270 | 271 | local forw,_ = S:GetAmplitudes('Substrate',0) 272 | local _,back = S:GetAmplitudes('Air',0) 273 | 274 | for _,order in ipairs(orders) do 275 | local ox,oy = unpack(order) 276 | local index = S:GetDiffractionOrder(ox,oy) 277 | local G = S:GetNumG() 278 | local complex_output_amplitude_fy = forw[index] 279 | local complex_output_amplitude_fx = forw[index + G] 280 | local complex_output_amplitude_ry = back[index] 281 | local complex_output_amplitude_rx = back[index + G] 282 | print(math.floor(wavelength*1000+0.5), pol, arg.ux, arg.uy, ox, oy, 283 | complex_output_amplitude_fy[1], complex_output_amplitude_fy[2], 284 | complex_output_amplitude_fx[1], complex_output_amplitude_fx[2], 285 | complex_output_amplitude_ry[1], complex_output_amplitude_ry[2], 286 | complex_output_amplitude_rx[1], complex_output_amplitude_rx[2]) 287 | end 288 | end 289 | 290 | function display_fom() 291 | --display figure of merit for optimizing gratings 292 | --order (short for target_diffraction_order) should be -1 for lens, 0 for light passing right through 293 | --inphase is whether or not we demand that the outgoing waves have consistent complex phase 294 | 295 | --[[ 296 | wavelengths_weights_orders_inphase = 297 | {{0.580, 1, -1, true}} 298 | --]] 299 | 300 | ---[[ 301 | wavelengths_weights_orders_inphase = 302 | {{0.580, 0.5, -1, true}, 303 | {0.450, 0.5, 0, true}} 304 | --]] 305 | 306 | --[[ 307 | wavelengths_weights_orders_inphase = 308 | {{0.500, 1, -1, false}, 309 | {0.580, 1, -1, true}, 310 | {0.650, 1, -1, false}} 311 | --]] 312 | 313 | score_so_far = 0 314 | weight_so_far = 0 315 | for i=1,#wavelengths_weights_orders_inphase,1 do 316 | local wavelength, weight, target_diffraction_order, inphase = unpack(wavelengths_weights_orders_inphase[i]) 317 | --right now I'm assuming that if we want light to pass through, we only care about normal incidence 318 | if target_diffraction_order ~= 0 then incident_theta = angle_in_air else incident_theta=0 end 319 | local S = set_up() 320 | local s = fom{pol='s', S=S, wavelength=wavelength,target_diffraction_order=target_diffraction_order, 321 | incident_theta=incident_theta, inphase=inphase} 322 | local p = fom{pol='p', S=S, wavelength=wavelength, target_diffraction_order=target_diffraction_order, 323 | incident_theta=incident_theta, inphase=inphase} 324 | --print('lam:', wavelength, 's:', s, 'p:', p) 325 | score_so_far = score_so_far + (s+p)/2 * weight 326 | weight_so_far = weight_so_far + weight 327 | end 328 | print(score_so_far / weight_so_far) 329 | 330 | --S:OutputLayerPatternDescription('Cylinders', 'temp/grating_img.ps') 331 | 332 | end 333 | 334 | if what_to_do == 'fom' then 335 | display_fom() 336 | do return end 337 | end 338 | 339 | function epsilon_map(S) 340 | local f = assert(io.open('temp/grating_eps.txt', 'w')) 341 | local z = cyl_height/2 342 | for x = -grating_period/2,grating_period/2,grating_period/100 do 343 | for y = -lateral_period/2,lateral_period/2,lateral_period/100 do 344 | local eps_r, eps_i = S:GetEpsilon({x,y,z}) 345 | f:write(x,' ',y,' ',z,' ',eps_r,' ',eps_i,'\n') 346 | end 347 | end 348 | end 349 | 350 | --epsilon_map(S) 351 | 352 | function print_fields(S, z) 353 | local num_x = 20 354 | local num_y = 20 355 | for ix=1,num_x,1 do 356 | local x = -grating_period/2 + (ix / num_x) * grating_period 357 | for iy=1,num_y,1 do 358 | local y = -lateral_period/2 + (iy / num_y) * lateral_period 359 | Exr, Eyr, Ezr, Hxr, Hyr, Hzr, Exi, Eyi, Ezi, Hxi, Hyi, Hzi = S:GetFields({x, y, z}) 360 | print(x, y, z, Exr, Eyr, Ezr, Hxr, Hyr, Hzr, Exi, Eyi, Ezi, Hxi, Hyi, Hzi) 361 | end 362 | end 363 | end 364 | 365 | 366 | function characterize(arg) 367 | --characterize a grating by calculating all the complex output amplitudes as a 368 | --function of incoming angle 369 | local S = set_up() 370 | local wavelength = arg.wavelength 371 | local kvac = 2*pi / wavelength 372 | local grating_kx = 2*pi / grating_period 373 | local grating_ky = 2*pi / lateral_period 374 | 375 | --ux,uy are the direction cosines, i.e. the x and y components of the unit vector in the direction of the incoming light 376 | local ux_min = arg.ux_min 377 | local ux_max = arg.ux_max 378 | local uy_min = arg.uy_min 379 | local uy_max = arg.uy_max 380 | num_steps = arg.num_steps 381 | 382 | for ix=0,num_steps-1,1 do 383 | local ux 384 | if num_steps == 1 then 385 | ux = (ux_min + ux_max)/2 386 | else 387 | ux = ux_min + ix * (ux_max - ux_min) / (num_steps-1) 388 | end 389 | local kx = kvac * ux 390 | for iy=0, num_steps-1,1 do 391 | local uy 392 | if num_steps == 1 then 393 | uy = (uy_min + uy_max)/2 394 | else 395 | uy = uy_min + iy * (uy_max - uy_min) / (num_steps-1) 396 | end 397 | local ky = kvac * uy 398 | if ux^2+uy^2 < 1 then 399 | local theta = math.acos((1-ux^2-uy^2)^0.5) 400 | local phi = math.atan2(uy,ux) 401 | --find the propagating diffraction orders (ox,oy), where (0,0) is by definition the 402 | --incoming wave. Exclude the light that propagates but 403 | --totally-internally-reflects, unless include_tir flag is true. For calculating the 404 | --far-field, we really don't need that stuff, but for near-field consistency checks 405 | --we do. 406 | local k_cutoff 407 | if arg.include_tir == true then 408 | if n_glass > 0 then 409 | k_cutoff = kvac * n_glass 410 | else 411 | k_cutoff = kvac * nSiO2_data[math.floor(wavelength*1000+0.5)] 412 | end 413 | else 414 | k_cutoff = kvac 415 | end 416 | local orders = {} 417 | for ox=-5,5,1 do 418 | for oy=-5,5,1 do 419 | if (kx + ox*grating_kx)^2 + (ky + oy*grating_ky)^2 < k_cutoff^2 then 420 | table.insert(orders, {ox,oy}) 421 | end 422 | end 423 | end 424 | for _, pol in ipairs({'s', 'p'}) do 425 | set_wavelength_angle{pol=pol, S=S, wavelength=wavelength, incident_theta=theta, incident_phi=phi} 426 | --ux and uy are sent only for display purposes, not used in the calculation 427 | print_orders{pol=pol, S=S, wavelength=wavelength, orders=orders, ux=ux, uy=uy} 428 | end 429 | end 430 | end 431 | end 432 | 433 | 434 | end 435 | 436 | -- UNCOMMENT THESE LINES FOR S4conventions.py, where I'm figuring out the relation 437 | -- that S4 uses to translate between complex amplitudes and real-space fields 438 | --[[ 439 | num_G = 50 440 | pol = 's' 441 | theta = math.asin((ux_min^2 + uy_min^2)^0.5) 442 | phi = math.atan2(uy_min, ux_min) 443 | characterize{num_G=num_G, ux_min=ux_min, ux_max=ux_max, uy_min=uy_min, 444 | uy_max=uy_max, num_steps=u_steps, wavelength=wavelength, include_tir=true} 445 | print('Fields') 446 | S = set_up() 447 | set_wavelength_angle{S=S, pol=pol, wavelength=wavelength, incident_theta=theta, incident_phi=phi} 448 | 449 | print_fields(S,-10.0) 450 | 451 | do return end 452 | --]] 453 | 454 | 455 | if what_to_do == 'characterize' then 456 | characterize{ux_min=ux_min, ux_max=ux_max, uy_min=uy_min, 457 | uy_max=uy_max, num_steps=u_steps, wavelength=wavelength} 458 | do return end 459 | end 460 | -------------------------------------------------------------------------------- /nearfield.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | (C) 2015 Steven Byrnes 4 | 5 | Calculate the near-field of a grating-based metasurface lens 6 | """ 7 | 8 | from __future__ import division, print_function 9 | import math 10 | from math import pi 11 | degree = pi / 180 12 | import numpy as np 13 | #http://pythonhosted.org/numericalunits/ 14 | import numericalunits as nu 15 | from numericalunits import um, nm 16 | # http://pythonhosted.org/dxfwrite/ 17 | from scipy.spatial import cKDTree 18 | import matplotlib.pyplot as plt 19 | inf = float('inf') 20 | 21 | 22 | import design_collimator 23 | import grating 24 | from numpy.fft import fft2, fftshift 25 | 26 | #import loop_post_analysis 27 | #import grating 28 | 29 | 30 | def good_fft_number(goal): 31 | """pick a number >= goal that has only factors of 2,3,5. FFT will be much 32 | faster if I use such a number""" 33 | assert goal < 1e5 34 | choices = [2**a * 3**b * 5**c for a in range(17) for b in range(11) 35 | for c in range(8)] 36 | return min(x for x in choices if x >= goal) 37 | 38 | """ 39 | * lens_periphery_summary, a dictionary: 40 | {'r_center_list': array([...]), 41 | -- the radius at the center of each subsequent grating 42 | 43 | 'r_min_list': array([...]), 44 | -- the radius at the inner boundary of this grating 45 | 46 | 'grating_period_list': array([...]), 47 | -- the period of the corresponding grating. Note that 48 | r_center_list[i] + 0.5 * grating_period_list[i] 49 | + 0.5 * grating_period_list[i+1] == r_center_list[i+1] 50 | 51 | 'gratingcollection_list': [...] 52 | -- GratingCollection objects from inside to out 53 | (list will look like [gc0, gc1, gc2, ...]) 54 | 55 | 'gratingcollection_index_here_list': [...] 56 | -- For each ring of gratings, what is the applicable 57 | gratingcollection object (indexed from the list above)? 58 | (list will look like [0,0,0,...,1,1,1,...2,2,2...]) 59 | 60 | 'num_around_circle_list': [...] 61 | -- how many copies are there around 2pi, for each entry of the above 62 | (list will look like [n1, n1, n1, ... , n2, n2, n2, ...]) 63 | } 64 | """ 65 | 66 | def build_nearfield(source_x, source_y, source_z, source_pol, wavelength, 67 | lens_periphery_summary, lens_center_summary, hexgridset, 68 | x_pts=None, y_pts=None, dipole_moment=1e-30 * nu.C * nu.m): 69 | """To get an isotropic source, we can take an incoherent sum of an x-, y-, 70 | and z-polarized dipole source. Then to get a Lambertian source we scale 71 | the field by cos(theta). So source_pol should be 'x' or 'y' or 'z'. I don't 72 | think there's any way to do it with just two incoherent runs ... you can't 73 | pick two orthogonal polarizations smoothly everywhere. 74 | 75 | Note: I am not worrying about how much RAM this function uses. If you run 76 | out of RAM just use build_nearfield_big() below instead. 77 | 78 | dipole_moment is arbitrary, it turns into a scale factor for E and H. But 79 | use real (numericalunits) units, and the result will also be in real units 80 | 81 | If source_z = -inf, do a normally-incident plane wave. Use dipole_moment 82 | as the magnitude of the electric field. 83 | """ 84 | assert source_z < 0 85 | assert source_pol in ('x','y','z') 86 | wavelength_in_nm = int(round(wavelength/nm)) 87 | r_min_list = lens_periphery_summary['r_min_list'] 88 | r_max_list = lens_periphery_summary['r_max_list'] 89 | r_center_list = lens_periphery_summary['r_center_list'] 90 | gratingcollection_index_here_list = lens_periphery_summary['gratingcollection_index_here_list'] 91 | num_around_circle_list = lens_periphery_summary['num_around_circle_list'] 92 | grating_period_list = lens_periphery_summary['grating_period_list'] 93 | gratingcollection_list = lens_periphery_summary['gratingcollection_list'] 94 | lens_max_r = r_max_list[-1] 95 | if x_pts is None: 96 | num_x = good_fft_number(2 * lens_max_r / (wavelength / 2.2)) 97 | x_pts = np.linspace(-lens_max_r, lens_max_r, num=num_x) 98 | else: 99 | num_x = len(x_pts) 100 | if y_pts is None: 101 | num_y = good_fft_number(2 * lens_max_r / (wavelength / 2.2)) 102 | y_pts = np.linspace(-lens_max_r, lens_max_r, num=num_y) 103 | else: 104 | num_y = len(y_pts) 105 | 106 | for l in [x_pts,y_pts]: 107 | diffs = [l[i+1] - l[i] for i in range(len(l)-1)] 108 | assert 0 < diffs[0] < wavelength/2 109 | assert max(diffs) - min(diffs) <= 1e-9 * max(abs(d) for d in diffs) 110 | 111 | n_glass = gratingcollection_list[0].grating_list[0].n_glass 112 | if n_glass == 0: 113 | n_glass = grating.n_glass(wavelength_in_nm) 114 | k_glass = 2*pi*n_glass/wavelength 115 | kvac = 2*pi/wavelength 116 | 117 | x_meshgrid,y_meshgrid = np.meshgrid(x_pts, y_pts, indexing='ij') 118 | lens_r = (x_meshgrid**2 + y_meshgrid**2)**0.5 119 | lens_phi = np.arctan2(y_meshgrid,x_meshgrid) 120 | 121 | # which_ring is the index for what ring of gratings each thing is, or -1 122 | # means N/A (in the center or outside the lens). in_center is specifically 123 | # points in the center 124 | 125 | ring_boundary_list = np.hstack((r_min_list, lens_max_r)) 126 | which_ring = np.searchsorted(ring_boundary_list, lens_r) - 1 127 | in_center = (which_ring == -1) 128 | which_ring[which_ring == len(r_min_list)] = -1 129 | 130 | if which_ring.max() == -1 and in_center.max() == 0: 131 | # no points in the lens, shortcut to the end 132 | Ex = Ey = Hx = Hy = np.zeros_like(which_ring, dtype=complex) 133 | power_passing_through_lens = 0 134 | return Ex, Ey, Hx, Hy, x_pts, y_pts, power_passing_through_lens, n_glass 135 | 136 | # #### test the which_ring code 137 | # for i,x in enumerate(x_pts): 138 | # for j,y in enumerate(y_pts): 139 | # n = which_ring[i,j] 140 | # r = (x**2 + y**2)**0.5 141 | # if n == -1: 142 | # assert r <= r_min_list[0] or r >= lens_max_r 143 | # else: 144 | # assert r_min_list[n] <= r <= r_max_list[n] 145 | 146 | # which_gratingcollection is -1 if the point is not in the periphery, or i 147 | # if it falls in the domain of gratingcollection_list[i] 148 | which_gratingcollection = gratingcollection_index_here_list[which_ring] 149 | which_gratingcollection[which_ring == -1] = -1 150 | 151 | 152 | # grating_period is the length of this grating unit cell in the radial 153 | # direction 154 | grating_period = grating_period_list[which_ring] 155 | # Note: The command a = blah[which_ring] will set a[i,j] = blah[-1] when 156 | # i,j is outside the lens periphery. I will not be using the data at these 157 | # points for any output results so it generally doesn't matter what they're 158 | # set to. (Except which_ring and which_gratingcollection; these are used to 159 | # see what's in the lens periphery.) 160 | 161 | # angle_per_grating the angle that you need to rotate about the lens 162 | # center to get to the next copy of this grating 163 | angle_per_grating = 2*pi/num_around_circle_list[which_ring] 164 | r_center = r_center_list[which_ring] 165 | # lateral_period is the length of this grating unit cell in the azimuthal 166 | # direction 167 | lateral_period = r_center * angle_per_grating 168 | # grating_rotation is the CCW rotation of this grating relative to the x axis 169 | grating_rotation = (lens_phi / angle_per_grating).round() * angle_per_grating 170 | gratingcenter_x = r_center * np.cos(grating_rotation) 171 | gratingcenter_y = r_center * np.sin(grating_rotation) 172 | dx = x_meshgrid - source_x 173 | dy = y_meshgrid - source_y 174 | dz = 0 - source_z 175 | distance = (dx**2 + dy**2 + dz**2)**0.5 176 | # (ux,uy,uz) is the unit vector that the incoming light is traveling. 177 | if source_z == -inf: 178 | ux = np.zeros_like(x_meshgrid) 179 | uy = np.zeros_like(x_meshgrid) 180 | uz = np.ones_like(x_meshgrid) 181 | else: 182 | ux = dx / distance 183 | uy = dy / distance 184 | uz = dz / distance 185 | 186 | # xp,yp,z (short for xprime, yprime,z) coordinates are a coordinate system 187 | # where (xp,yp)=(0,0) is the center of the grating that this point is on, 188 | # increasing xp moves away from the lens center, and increasing yp move 189 | # CCW around the lens center. 190 | 191 | # (uxp,uyp,uz) is the primed coordinates version of (ux,uy,uz), i.e. the 192 | # unit vector that the incoming light is travelgin 193 | # Checking signs: If (ux,uy)=(1,0) (light heading rightward) 194 | # and grating_rotation = +10degrees (first quadrant) then uyp is negative 195 | uxp = ux * np.cos(grating_rotation) + uy * np.sin(grating_rotation) 196 | uyp = -ux * np.sin(grating_rotation) + uy * np.cos(grating_rotation) 197 | # Checking signs: If (x,y) ~ (cos(grating_rotation),sin(grating_rotation)) 198 | # then we expect yp = 0 199 | # The following two options are exactly identical (I checked) 200 | xp = x_meshgrid * np.cos(grating_rotation) + y_meshgrid * np.sin(grating_rotation) - r_center 201 | yp = -x_meshgrid * np.sin(grating_rotation) + y_meshgrid * np.cos(grating_rotation) 202 | # xp = ((x_meshgrid-gratingcenter_x) * np.cos(grating_rotation) 203 | # + (y_meshgrid-gratingcenter_y) * np.sin(grating_rotation)) 204 | # yp = (-(x_meshgrid-gratingcenter_x) * np.sin(grating_rotation) 205 | # + (y_meshgrid-gratingcenter_y) * np.cos(grating_rotation)) 206 | 207 | 208 | # dipole field: We are calculating the actual field in real units, except 209 | # for the e^ikr phase factor 210 | # lambert cosine law: intensity goes as cos(angle_from_normal), so I should 211 | # scale fields by the square-root of that, i.e. uz**0.5 212 | # Jackson (9.19): H = ck^2/4pi * (n x p) * e^ikr/r ; E = Z0 H x n 213 | H_coef = nu.c0 * (2*pi / wavelength)**2 * dipole_moment / (4*pi) 214 | 215 | pol_vector = {'x':[1,0,0], 'y':[0,1,0], 'z':[0,0,1]}[source_pol] 216 | if source_z > -inf: 217 | dipole_field_Hx = (uy * pol_vector[2] - uz * pol_vector[1]) * H_coef * uz**0.5 / distance 218 | dipole_field_Hy = (uz * pol_vector[0] - ux * pol_vector[2]) * H_coef * uz**0.5 / distance 219 | dipole_field_Hz = (ux * pol_vector[1] - uy * pol_vector[0]) * H_coef * uz**0.5 / distance 220 | # then E is proportional to H cross rhat 221 | dipole_field_Ex = (dipole_field_Hy * uz - dipole_field_Hz * uy) * nu.Z0 222 | dipole_field_Ey = (dipole_field_Hz * ux - dipole_field_Hx * uz) * nu.Z0 223 | else: 224 | assert source_pol != 'z' 225 | dipole_field_Ex = pol_vector[0] * dipole_moment * np.ones((num_x,num_y)) 226 | dipole_field_Ey = pol_vector[1] * dipole_moment * np.ones((num_x,num_y)) 227 | dipole_field_Hx = -pol_vector[1] * dipole_moment / nu.Z0 * np.ones((num_x,num_y)) 228 | dipole_field_Hy = pol_vector[0] * dipole_moment / nu.Z0 * np.ones((num_x,num_y)) 229 | 230 | # switch to primed coordinates 231 | dipole_field_Hxp = (dipole_field_Hx * np.cos(grating_rotation) 232 | + dipole_field_Hy * np.sin(grating_rotation)) 233 | dipole_field_Hyp = (-dipole_field_Hx * np.sin(grating_rotation) 234 | + dipole_field_Hy * np.cos(grating_rotation)) 235 | 236 | 237 | # Our grating.characterize() data has results of a simulation with unit 238 | # amplitude x-polarized incoming light, and a simulation with y-polarized 239 | # (see S4conventions.py for definitions). We want to write our incoming 240 | # dipole_field as 241 | # x_weight * (x simulation incoming field) + y_weight * (y incoming field) 242 | # and then we know that the output is similarly a sum of the two simulation 243 | # outputs. 244 | # Note that this is the weight for H. H_weight * Z0 == E_weight, because 245 | # Z0=1 in S4 units (Z0 is impedance of free space) 246 | H_xp_weight = dipole_field_Hyp 247 | H_yp_weight = dipole_field_Hxp 248 | 249 | # electric and magnetic fields in primed coordinates at each point 250 | # There is a z component too but it doesn't enter far-field calculation 251 | Exp = np.zeros((num_x,num_y), dtype=complex) 252 | Eyp = np.zeros((num_x,num_y), dtype=complex) 253 | Hxp = np.zeros((num_x,num_y), dtype=complex) 254 | Hyp = np.zeros((num_x,num_y), dtype=complex) 255 | 256 | # This does the interpolation. Note that we are evaluating each 257 | # interpolating function only once, in a vectorized way, otherwise it is 258 | # super slow. 259 | # make cache to store kxp, kyp, kxp**2+kyp**2 for each grating order 260 | kxp_cache = {} 261 | kyp_cache = {} 262 | kxp2_plus_kyp2_cache = {} 263 | for gc_index, gc in enumerate(gratingcollection_list): 264 | all_orders = {(e['ox'],e['oy']) for g in gc.grating_list for e in g.data} 265 | for ox,oy in all_orders: 266 | # uxp,uyp is propagation direction in air. So use kvac here, not kglass 267 | if (ox,oy) not in kxp_cache: 268 | kxp = kvac * uxp + ox * 2*pi/grating_period 269 | kyp = kvac * uyp + oy * 2*pi/lateral_period 270 | kxp2_plus_kyp2 = kxp**2 + kyp**2 271 | kxp_cache[(ox,oy)] = kxp 272 | kyp_cache[(ox,oy)] = kyp 273 | kxp2_plus_kyp2_cache[(ox,oy)] = kxp2_plus_kyp2 274 | else: 275 | kxp = kxp_cache[(ox,oy)] 276 | kyp = kyp_cache[(ox,oy)] 277 | kxp2_plus_kyp2 = kxp2_plus_kyp2_cache[(ox,oy)] 278 | 279 | entries = np.logical_and((kxp2_plus_kyp2 <= kvac**2), 280 | (which_gratingcollection==gc_index)) 281 | if entries.sum() == 0: 282 | continue 283 | print('diffraction order', (ox,oy), 'of gc', gc_index, 284 | '; applies at', entries.sum(), 'points', flush=True) 285 | kxp = kxp[entries] 286 | kyp = kyp[entries] 287 | kzp = (k_glass**2-kxp**2-kyp**2)**0.5 288 | # S4 references phases to the pillar-glass interface, center of the 289 | # grating unit cell. Because we want the field at a different point, 290 | # we need a phase propagation factor 291 | phase_from_offcenter = np.exp(1j * (kxp * xp[entries] + kyp * yp[entries])) 292 | 293 | points_to_interpolate_at = np.vstack((uxp[entries], uyp[entries], grating_period[entries])).T 294 | if uxp[entries].min() < gc.interpolator_bounds[0]: 295 | raise ValueError('need to calculate at smaller ux!', uxp[entries].min(), gc.interpolator_bounds[0]) 296 | if uxp[entries].max() > gc.interpolator_bounds[1]: 297 | raise ValueError('need to calculate at bigger ux!', uxp[entries].max(), gc.interpolator_bounds[1]) 298 | if uyp[entries].min() < gc.interpolator_bounds[2]: 299 | raise ValueError('need to calculate at smaller uy!', uyp[entries].min(), gc.interpolator_bounds[2]) 300 | if uyp[entries].max() > gc.interpolator_bounds[3]: 301 | raise ValueError('need to calculate at bigger uy!', uyp[entries].max(), gc.interpolator_bounds[3]) 302 | if grating_period[entries].min() < gc.interpolator_bounds[4]: 303 | raise ValueError('need to calculate at smaller grating_period!', grating_period[entries].min()/nm, gc.interpolator_bounds[4]/nm) 304 | if grating_period[entries].max() > gc.interpolator_bounds[5]: 305 | raise ValueError('need to calculate at bigger grating_period!', grating_period[entries].max()/nm, gc.interpolator_bounds[5]/nm) 306 | for x_or_y in ('x', 'y'): 307 | H_weight = H_xp_weight[entries] if x_or_y == 'x' else H_yp_weight[entries] 308 | E_weight = H_weight * nu.Z0 309 | for which_amp in ('ampfy', 'ampfx'): 310 | f = gc.interpolators[(wavelength_in_nm, (ox,oy), x_or_y, which_amp)] 311 | amps = f(points_to_interpolate_at) 312 | if which_amp == 'ampfy': 313 | Exp[entries] += (E_weight * amps 314 | * kxp * kyp / (k_glass * kzp) / n_glass 315 | * phase_from_offcenter) 316 | Eyp[entries] += (E_weight * amps 317 | * (-kxp**2 - kzp**2) / (k_glass * kzp) / n_glass 318 | * phase_from_offcenter) 319 | Hxp[entries] += H_weight * amps * phase_from_offcenter 320 | else: 321 | Exp[entries] += (E_weight * amps 322 | * (kyp**2 + kzp**2) / (k_glass * kzp) / n_glass 323 | * phase_from_offcenter) 324 | Eyp[entries] += (E_weight * amps 325 | * -kxp*kyp / (k_glass * kzp) / n_glass 326 | * phase_from_offcenter) 327 | Hyp[entries] += H_weight * amps * phase_from_offcenter 328 | 329 | 330 | 331 | 332 | 333 | # note that the S4 individual grating simulations assume the light has 334 | # phase 0 at (x,y)=grating_center, z=air-pillar interface. 335 | # Note also that S4 propagates using e^{+ikr} 336 | # Remember, in dipole_field_Hx etc., we included everything but e^ikr 337 | if source_z > -inf: 338 | air_propagation_distance = ((gratingcenter_x - source_x)**2 339 | + (gratingcenter_y - source_y)**2 340 | + source_z**2)**0.5 341 | eikr = np.exp(1j * kvac * air_propagation_distance) 342 | Exp *= eikr 343 | Eyp *= eikr 344 | #Ez *= eikr 345 | Hxp *= eikr 346 | Hyp *= eikr 347 | #Hz *= eikr 348 | 349 | # double-check signs: If grating_rotation=10deg (first quadrant) and 350 | # Exp=1, Eyp=0 (E points outward), then Ex>0,Ey>0 351 | Ex = Exp * np.cos(grating_rotation) - Eyp * np.sin(grating_rotation) 352 | Ey = Exp * np.sin(grating_rotation) + Eyp * np.cos(grating_rotation) 353 | Hx = Hxp * np.cos(grating_rotation) - Hyp * np.sin(grating_rotation) 354 | Hy = Hxp * np.sin(grating_rotation) + Hyp * np.cos(grating_rotation) 355 | # Note E=H=0 outside lens periphery 356 | 357 | ############ Next, the center part of the lens! ############### 358 | 359 | x = x_meshgrid[in_center] 360 | y = y_meshgrid[in_center] 361 | # closest_indices[j] is the index of the entry in lens_center_summary 362 | # that is closest to (x[j],y[j]) 363 | mytree = cKDTree(lens_center_summary[:,0:2]) 364 | closest_indices = mytree.query(np.vstack((x,y)).T)[1] 365 | cell_center_x = lens_center_summary[closest_indices, 0] 366 | cell_center_y = lens_center_summary[closest_indices, 1] 367 | which_grating = lens_center_summary[closest_indices, 2].astype(int) 368 | 369 | Ex_centerpoints = np.zeros_like(x, dtype=complex) 370 | Ey_centerpoints = np.zeros_like(x, dtype=complex) 371 | Hx_centerpoints = np.zeros_like(x, dtype=complex) 372 | Hy_centerpoints = np.zeros_like(x, dtype=complex) 373 | 374 | # how much to weight the results with x-polarized and y-polarized input 375 | H_x_weight = dipole_field_Hy 376 | H_y_weight = dipole_field_Hx 377 | 378 | if source_z > -inf: 379 | dx = x - source_x 380 | dy = y - source_y 381 | dz = 0 - source_z 382 | distance = (dx**2 + dy**2 + dz**2)**0.5 383 | # (ux,uy,uz) is the unit vector that the incoming light is traveling. 384 | ux = dx / distance 385 | uy = dy / distance 386 | uz = dz / distance 387 | else: 388 | ux = uy = np.zeros_like(x) 389 | uz = np.ones_like(x) 390 | all_orders = {(e['ox'],e['oy']) for g in hexgridset.grating_list for e in g.data} 391 | x_period = hexgridset.grating_list[0].grating_period 392 | y_period = hexgridset.grating_list[0].lateral_period 393 | for ox,oy in all_orders: 394 | # ux,uy is propagation direction in air. So use kvac here, not kglass 395 | kx = kvac * ux + ox * 2*pi/x_period 396 | ky = kvac * uy + oy * 2*pi/y_period 397 | 398 | entries = (kx**2 + ky**2 <= kvac**2) 399 | if entries.sum() == 0: 400 | continue 401 | print('diffraction order', (ox,oy), 'of center; applies at', entries.sum(), 'points', flush=True) 402 | kx = kx[entries] 403 | ky = ky[entries] 404 | kz = (k_glass**2-kx**2-ky**2)**0.5 405 | # S4 references phases to the pillar-glass interface, center of the 406 | # grating unit cell. Because we want the field at a different point, 407 | # we need a phase propagation factor 408 | phase_from_offcenter = np.exp(1j * (kx * (x[entries] - cell_center_x[entries]) 409 | + ky * (y[entries] - cell_center_y[entries]))) 410 | 411 | points_to_interpolate_at = np.vstack((ux[entries], uy[entries], which_grating[entries])).T 412 | if ux[entries].min() < hexgridset.interpolator_bounds[0]: 413 | raise ValueError('need to calculate at smaller ux!', ux[entries].min(), hexgridset.interpolator_bounds[0]) 414 | if ux[entries].max() > hexgridset.interpolator_bounds[1]: 415 | raise ValueError('need to calculate at bigger ux!', ux[entries].max(), hexgridset.interpolator_bounds[1]) 416 | if uy[entries].min() < hexgridset.interpolator_bounds[2]: 417 | raise ValueError('need to calculate at smaller uy!', uy[entries].min(), hexgridset.interpolator_bounds[2]) 418 | if uy[entries].max() > hexgridset.interpolator_bounds[3]: 419 | raise ValueError('need to calculate at bigger uy!', uy[entries].max(), hexgridset.interpolator_bounds[3]) 420 | for x_or_y in ('x', 'y'): 421 | H_weight = H_x_weight[in_center][entries] if x_or_y == 'x' else H_y_weight[in_center][entries] 422 | E_weight = H_weight * nu.Z0 423 | for which_amp in ('ampfy', 'ampfx'): 424 | f = hexgridset.interpolators[(wavelength_in_nm, (ox,oy), x_or_y, which_amp)] 425 | amps = f(points_to_interpolate_at) 426 | if which_amp == 'ampfy': 427 | Ex_centerpoints[entries] += (E_weight * amps 428 | * kx * ky / (k_glass * kz) / n_glass 429 | * phase_from_offcenter) 430 | Ey_centerpoints[entries] += (E_weight * amps 431 | * (-kx**2 - kz**2) / (k_glass * kz) / n_glass 432 | * phase_from_offcenter) 433 | Hx_centerpoints[entries] += H_weight * amps * phase_from_offcenter 434 | else: 435 | Ex_centerpoints[entries] += (E_weight * amps 436 | * (ky**2 + kz**2) / (k_glass * kz) / n_glass 437 | * phase_from_offcenter) 438 | Ey_centerpoints[entries] += (E_weight * amps 439 | * -kx*ky / (k_glass * kz) / n_glass 440 | * phase_from_offcenter) 441 | Hy_centerpoints[entries] += H_weight * amps * phase_from_offcenter 442 | # temp = x_meshgrid*0 443 | # temp2 = temp[in_center] 444 | # temp2[entries] += amps 445 | # temp[in_center] += temp2 446 | # #temp[in_center][entries] = E_weight 447 | # plt.figure() 448 | # plt.imshow(temp.T) 449 | # plt.title(s_or_p + ' ' + which_amp + ' ' + str((ox,oy))) 450 | # plt.colorbar() 451 | # 452 | 453 | if source_z > -inf: 454 | air_propagation_distance = ((cell_center_x - source_x)**2 455 | + (cell_center_y - source_y)**2 456 | + source_z**2)**0.5 457 | eikr = np.exp(1j * kvac * air_propagation_distance) 458 | Ex_centerpoints *= eikr 459 | Ey_centerpoints *= eikr 460 | Hx_centerpoints *= eikr 461 | Hy_centerpoints *= eikr 462 | 463 | Ex[in_center] += Ex_centerpoints 464 | Ey[in_center] += Ey_centerpoints 465 | Hx[in_center] += Hx_centerpoints 466 | Hy[in_center] += Hy_centerpoints 467 | 468 | # a = Ex / (dipole_field_Ex*np.exp(1j * kvac * ((x_meshgrid-source_x)**2 + (y_meshgrid-source_y)**2 + source_z**2)**0.5)) 469 | # plt.figure() 470 | # plt.imshow(a.real.T) 471 | # plt.colorbar() 472 | 473 | # TODO - Check for Possible factor-of-2 error?? 474 | local_power_z = dipole_field_Ex * dipole_field_Hy - dipole_field_Ey * dipole_field_Hx 475 | entries = np.logical_or((which_gratingcollection != -1), in_center) 476 | power_passing_through_lens = (local_power_z[entries].sum() 477 | * (x_pts[1]-x_pts[0]) * (y_pts[1]-y_pts[0])) 478 | 479 | 480 | return Ex, Ey, Hx, Hy, x_pts, y_pts, power_passing_through_lens, n_glass 481 | 482 | def build_nearfield_big(source_x, source_y, source_z, source_pol, wavelength, 483 | lens_periphery_summary, lens_center_summary, hexgridset, 484 | x_pts=None, y_pts=None, dipole_moment=1e-30 * nu.C * nu.m): 485 | """build_nearfield() uses a lot of temporary storage. With lots of 486 | near-field points, this function avoids running out of memory by filling in 487 | a subset of the points at a time""" 488 | pts_at_a_time = 1e7 489 | y_pts_at_a_time = int(pts_at_a_time / x_pts.size) 490 | 491 | Ex = np.zeros(shape=(x_pts.size,y_pts.size), dtype=complex) 492 | Ey = np.zeros(shape=(x_pts.size,y_pts.size), dtype=complex) 493 | Hx = np.zeros(shape=(x_pts.size,y_pts.size), dtype=complex) 494 | Hy = np.zeros(shape=(x_pts.size,y_pts.size), dtype=complex) 495 | power_passing_through_lens=0 496 | 497 | start = 0 498 | end = min(start+y_pts_at_a_time, y_pts.size) 499 | while start < y_pts.size: 500 | print('running y-index', start, 'to', end, 'out of', y_pts.size, flush=True) 501 | y_pts_now = y_pts[start:end] 502 | Ex_now,Ey_now,Hx_now,Hy_now,_,_,P_now,n_glass = build_nearfield( 503 | source_x=source_x, source_y=source_y, source_z=source_z, 504 | source_pol=source_pol, wavelength=wavelength, 505 | lens_periphery_summary=lens_periphery_summary, 506 | lens_center_summary=lens_center_summary, hexgridset=hexgridset, 507 | x_pts=x_pts, y_pts=y_pts_now, dipole_moment=dipole_moment) 508 | Ex[:, start:end] = Ex_now 509 | Ey[:, start:end] = Ey_now 510 | Hx[:, start:end] = Hx_now 511 | Hy[:, start:end] = Hy_now 512 | power_passing_through_lens += P_now 513 | start = end 514 | end = min(start+y_pts_at_a_time, y_pts.size) 515 | 516 | return Ex, Ey, Hx, Hy, x_pts, y_pts, power_passing_through_lens, n_glass 517 | 518 | -------------------------------------------------------------------------------- /grating.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | (C) 2015 Steven Byrnes 4 | 5 | Design and optimize metasurface gratings. 6 | 7 | """ 8 | import math, os, matplotlib, shutil 9 | import numpy as np 10 | from numpy import array 11 | import matplotlib.pyplot as plt 12 | import subprocess 13 | from subprocess import PIPE 14 | import random 15 | import cmath 16 | import string 17 | from scipy.interpolate import RegularGridInterpolator 18 | 19 | # my units package 20 | # https://pypi.python.org/pypi/numericalunits 21 | # just run the command "pip install numericalunits" 22 | import numericalunits as nu 23 | from numericalunits import m, nm, um 24 | 25 | pi = math.pi 26 | inf = float('inf') 27 | degree = pi / 180 28 | 29 | # we assume that S4 and grating.lua are in the same folder as this python file. 30 | # S4 will look for the inputs files (i.e., files specifying cylinder locations 31 | # and other parameters) in a subfolder of the current working directory (cwd) 32 | # called "temp". We can set the cwd to whatever we want in order to run multiple 33 | # optimizations simultaneously (generally, each in its own independent IPython 34 | # interpreter, though some parts of the python code will spawn multiple 35 | # subprocesses that run in parallel.) 36 | 37 | def cwd_for_S4(subfolder=None): 38 | """cwd (current working directory) in which to run S4. Use a subfolder 39 | if running multiple instances in parallel. Subfolder can be any string.""" 40 | here = os.path.dirname(os.path.realpath('__file__')) 41 | if subfolder is None: 42 | return here 43 | else: 44 | return os.path.join(here, 'temp', subfolder) 45 | 46 | def path_to_temp(subfolder=None): 47 | """cwd of S4 has a subfolder named "temp" where the data-files go. Also, 48 | this function creates relevant folders if they don't already exist, and 49 | copies grating.lua if necessary""" 50 | path = os.path.join(cwd_for_S4(subfolder), 'temp') 51 | if subfolder is not None: 52 | if not os.path.exists(path): 53 | os.makedirs(path) 54 | src = os.path.join(cwd_for_S4(), 'grating.lua') 55 | dst = os.path.join(cwd_for_S4(subfolder), 'grating.lua') 56 | if (not os.path.isfile(dst) or os.path.getmtime(src) > os.path.getmtime(dst)): 57 | shutil.copyfile(src=src, dst=dst) 58 | return path 59 | 60 | def remove_subfolder(subfolder): 61 | assert subfolder not in (None, '') 62 | shutil.rmtree(cwd_for_S4(subfolder)) 63 | 64 | def random_subfolder_name(): 65 | return ''.join(random.choice(string.ascii_letters + string.digits) 66 | for _ in range(6)) 67 | 68 | def xyrra_filename(subfolder=None, index=None): 69 | """This file is used to communicate the posititons of the cylinders to 70 | S4 or lumerical. 71 | Use a subfolder to run many S4 optimizations at once. 72 | The index is for running a batch of calculations using lumerical.""" 73 | filename = ('grating_xyrra_list' 74 | + (str(index) if index is not None else '') 75 | + '.txt') 76 | return os.path.join(path_to_temp(subfolder), filename) 77 | 78 | def setup_filename(subfolder=None, index=None): 79 | """This file is used to communicate how the simulation is set up to 80 | S4 or lumerical. 81 | Use a subfolder to run many S4 optimizations at once. 82 | The index is for running a batch of calculations using lumerical.""" 83 | filename = ('grating_setup' 84 | + (str(index) if index is not None else '') 85 | + '.txt') 86 | return os.path.join(path_to_temp(subfolder), filename) 87 | 88 | 89 | 90 | class Grating: 91 | """A Grating object has all the information needed to specify a grating. 92 | including the locations and sizes of the cylinders and the periodicity. 93 | 94 | * xyrra_list is a list of [x at center, y at center, semi-axis in x 95 | direction, semi-axis in y direction, rotation angle (positive is CCW)] for 96 | each ellipse in the pattern. Internally, it's stored with lengths in 97 | "numericalunits" (see https://pypi.python.org/pypi/numericalunits) and 98 | angles are in radians. 99 | 100 | * cyl_height is the height of each nano-pillar 101 | 102 | * The periodic cell has size grating_period × lateral_period. The former is 103 | larget and is supposed to bend light to the +1 diffraction order (or -1 104 | depending on your conventions.) 105 | 106 | * n_glass and n_tio2 are refractive index of the substrate and pillars 107 | respectively. Set to 0 to use measured / literature dispersion curves. 108 | 109 | Instead of specifying grating_period directly, you can also specify 110 | angle_in_air, the angle that light is traveling while in the air 111 | (relative to normal), before hitting the meta-lens and traveling through 112 | glass (hopefully) at normal. Then you need to also specify 113 | target_wavelength. 114 | """ 115 | def __init__(self, lateral_period, cyl_height, grating_period=None, 116 | target_wavelength=None, angle_in_air=None, 117 | n_glass=0, n_tio2=0, xyrra_list_in_nm_deg=None, data=None): 118 | """supply EITHER grating_period OR (angle_in_air and target_wavelength) 119 | which allows you to compute grating_period. But grating_period is the 120 | only parameter saved as a property, not angle_in_air nor target_wavelength. 121 | 122 | Set n_glass and/or n_tio2 to zero to use tabulated values""" 123 | if grating_period is not None: 124 | assert (target_wavelength is None) and (angle_in_air is None) 125 | self.grating_period = grating_period 126 | else: 127 | self.grating_period = target_wavelength / math.sin(angle_in_air) 128 | self.n_glass = n_glass 129 | self.n_tio2 = n_tio2 130 | self.lateral_period = lateral_period 131 | self.cyl_height = cyl_height 132 | 133 | self.grating_kx = 2*pi / self.grating_period 134 | 135 | if xyrra_list_in_nm_deg is not None: 136 | self.xyrra_list = xyrra_list_in_nm_deg.copy() 137 | self.xyrra_list[:,0:4] *= nm 138 | self.xyrra_list[:,4] *= degree 139 | if data is not None: 140 | self.data = data 141 | 142 | def get_xyrra_list(self, units=None, replicas=None): 143 | """Get a copy of the xyrra_list. 144 | 145 | For units, None means "as stored interally", or 'um,deg' means xyrr 146 | are in microns and a is in degrees, or 'nm,deg' is similar. 147 | 148 | For replicas: For python internal storage and for lua, we only need one 149 | periodic replica of each ellipse, whereas for lumerical and display we 150 | need multiple periodic replicas. With replicas=True, include every 151 | ellipse that sticks into the central unit cell. With replicas=N, 152 | include every ellipse that is within N unit cells of the center. (To 153 | allow zooming out in a display.)""" 154 | if replicas is not None: 155 | # N is number of unit cells away from center to include 156 | N = (0 if replicas is True else replicas) 157 | grating_period = self.grating_period 158 | lateral_period = self.lateral_period 159 | new_xyrra_list = [] 160 | for x,y,rx,ry,a in self.xyrra_list: 161 | for translate_x in range(-(N+1), N+2): 162 | for translate_y in range(-(N+1), N+2): 163 | x_center = x + translate_x * grating_period 164 | y_center = y + translate_y * lateral_period 165 | ellipse_pt_list = ellipse_pts(x_center, y_center, rx, ry, a, num_points=120) 166 | if any(abs(x) < grating_period/2 + N*grating_period 167 | and abs(y) < lateral_period/2 + N*lateral_period 168 | for x,y in ellipse_pt_list): 169 | new_xyrra_list.append([x_center, y_center, rx, ry, a]) 170 | new_xyrra_list = np.array(new_xyrra_list) 171 | else: 172 | new_xyrra_list = self.xyrra_list.copy() 173 | if units is None: 174 | return new_xyrra_list 175 | if units == 'nm,deg': 176 | new_xyrra_list[:,0:4] /= nm 177 | new_xyrra_list[:,4] /= degree 178 | return new_xyrra_list 179 | if units == 'um,deg': 180 | new_xyrra_list[:,0:4] /= um 181 | new_xyrra_list[:,4] /= degree 182 | return new_xyrra_list 183 | raise ValueError('bad units specification') 184 | 185 | @property 186 | def xyrra_list_in_nm_deg(self): 187 | """Expresses x,y,r,r in nm and angle in degrees""" 188 | return self.get_xyrra_list(units='nm,deg') 189 | 190 | @property 191 | def xyrra_list_in_um_deg(self): 192 | """Expresses x,y,r,r in microns and angle in degrees""" 193 | return self.get_xyrra_list(units='um,deg') 194 | 195 | def get_angle_in_air(self, target_wavelength): 196 | """If this grating were part of a lens designed for target_wavelength, 197 | then it would be located at a place where light in air is going at 198 | this angle. (Then the light would be bent to normal in glass.)""" 199 | if self.grating_period < target_wavelength: 200 | raise ValueError('bad inputs!', target_wavelength/nm, self.grating_period/nm) 201 | return math.asin(target_wavelength / self.grating_period) 202 | 203 | def write(self, angle_in_air=None, subfolder=None, index=None, replicas=False, 204 | ux_min=None, ux_max=None, uy_min=None, uy_max=None, u_steps=None, 205 | wavelength=None, numG=50): 206 | """save simulation setup and xyrra_list to a standard file, which 207 | either lumerical or S4 can read. 208 | 209 | The "replicas" parameter is defined as in self.get_xyrra_list(). TODO - need for lumerical 210 | 211 | angle_in_air is the angle from which the light is coming at the 212 | metasurface. In radians. 213 | 214 | numG is the number of modes used in RCWA. More is slower but more 215 | accurate. 216 | 217 | index is appended to the filename, this is used for batch lumerical simulations. 218 | 219 | The following parameters are used ONLY for far-field calculation, not 220 | figure-of-merit for optimizations: 221 | * wavelength is self-explanatory (in numericalunits) 222 | * ux_min, ux_max, uy_min, uy_max is the range of incident light angles 223 | to test ... the light is traveling in the direction with unit vector 224 | (ux,uy,uz). 225 | * u_steps is the number of increments for varying ux and uy (each is 226 | the same, at least for now) 227 | 228 | """ 229 | filename = setup_filename(subfolder=subfolder, index=index) 230 | with open(filename, 'w') as f: 231 | if angle_in_air is not None: 232 | # calculate figure-of-merit 233 | assert all(x is None for x in (ux_min, ux_max, uy_min, uy_max, 234 | u_steps, wavelength)) 235 | print(1, file=f) 236 | print(self.grating_period / m, file=f) 237 | print(self.lateral_period / m, file=f) 238 | print(angle_in_air, file=f) 239 | print(self.n_glass, file=f) 240 | print(self.n_tio2, file=f) 241 | print(self.cyl_height / m, file=f) 242 | print(numG, file=f) 243 | else: 244 | assert all(x is not None for x in (ux_min, ux_max, uy_min, uy_max, 245 | u_steps, wavelength)) 246 | print(2, file=f) 247 | print(self.grating_period / m, file=f) 248 | print(self.lateral_period / m, file=f) 249 | print(self.n_glass, file=f) 250 | print(self.n_tio2, file=f) 251 | print(self.cyl_height / m, file=f) 252 | print(numG, file=f) 253 | print(ux_min, file=f) 254 | print(ux_max, file=f) 255 | print(uy_min, file=f) 256 | print(uy_max, file=f) 257 | print(u_steps, file=f) 258 | print(round(wavelength/nm)/1000, file=f) 259 | 260 | np.savetxt(xyrra_filename(subfolder=subfolder, index=index), 261 | self.xyrra_list_in_um_deg, delimiter=' ') 262 | 263 | def __repr__(self): 264 | """this is important - after we spend 5 minutes finding a nice 265 | Grating, let's say "mygrating", we can just type 266 | "mygrating" or "repr(mygrating)" into IPython and copy the 267 | resulting text. That's the code to re-create that grating 268 | immediately, without re-calculating it.""" 269 | xyrra_list_str = (np.array2string(self.xyrra_list_in_nm_deg, separator=',') 270 | .replace(' ', '').replace('\n','')) 271 | return ('Grating(lateral_period=' + repr(self.lateral_period/nm) + '*nm' 272 | + ', grating_period=' + repr(self.grating_period/nm) + '*nm' 273 | + ', cyl_height=' + repr(self.cyl_height/nm) + '*nm' 274 | + ', n_glass=' + repr(self.n_glass) 275 | + ', n_tio2=' + repr(self.n_tio2) 276 | + ', xyrra_list_in_nm_deg=np.array(' + xyrra_list_str + ')' 277 | + ', data=' + (repr(self.data) if hasattr(self,'data') else 'None') 278 | + ')') 279 | 280 | def copy(self): 281 | return eval(repr(self)) 282 | 283 | def run_lua(self, target_wavelength=None, subfolder=None, **kwargs): 284 | """output the parameters for a simulation, then run the lua S4 script and 285 | return the result. 286 | Target_wavelength is important for determining what angle the light is 287 | coming at. (For collimator applications, this angle applies to all 288 | wavelengths.) This is only used for calculating figure-of-merit; for 289 | characterize() leave it out 290 | To run many in parallel, use run_lua_initiate() and run_lua_getresult() 291 | separately. The former starts the simulation running but doesn't wait 292 | for it to finish; instead it returns the process (Popen) object. Then 293 | the latter reads the result. 294 | kwargs are optional arguments passed through to self.write(). Used for 295 | self.characterize()""" 296 | process = self.run_lua_initiate(target_wavelength=target_wavelength, 297 | subfolder=subfolder, **kwargs) 298 | return self.run_lua_getresult(process) 299 | 300 | def run_lua_initiate(self, target_wavelength=None, subfolder=None, **kwargs): 301 | """See self.run_lua() for information""" 302 | angle_in_air = None if target_wavelength is None else self.get_angle_in_air(target_wavelength) 303 | self.write(angle_in_air=angle_in_air, subfolder=subfolder, **kwargs) 304 | cwd = cwd_for_S4(subfolder=subfolder) 305 | return subprocess.Popen(['S4', 'grating.lua'], 306 | cwd=cwd, stdout=PIPE, stderr=PIPE) 307 | 308 | def run_lua_getresult(self, process): 309 | """See self.run_lua() for information""" 310 | output, error = process.communicate() 311 | try: 312 | return float(output) 313 | except ValueError: 314 | print('Cannot convert output to float!') 315 | print('S4 output was:', repr(output.decode())) 316 | print('S4 error was:', repr(error.decode())) 317 | return 0 318 | 319 | def run_lumerical(self, target_wavelength): 320 | """output the parameters for a simulation. Can't actually run the 321 | simulation though, need to open grating_lumerical.lsf and run it manually, 322 | as far as I know. 323 | Target_wavelength is important for determining what angle the light is 324 | coming at. (For collimator applications, this angle applies to all 325 | wavelengths.)""" 326 | angle_in_air = self.get_angle_in_air(target_wavelength) 327 | self.write(angle_in_air=angle_in_air, index=0, replicas=True) 328 | if os.path.isfile(xyrra_filename(index=1)): 329 | os.remove(xyrra_filename(index=1)) 330 | os.remove(setup_filename(index=1)) 331 | 332 | def standardize(self): 333 | """ pick the appropriate periodic replica of each item. Overwrite original """ 334 | grating_period = self.grating_period 335 | lateral_period = self.lateral_period 336 | xyrra_list = self.xyrra_list 337 | xyrra_list[:,0] %= grating_period 338 | xyrra_list[(xyrra_list[:,0]>grating_period/2),0] -= grating_period 339 | xyrra_list[:,1] %= lateral_period 340 | xyrra_list[(xyrra_list[:,1]>lateral_period/2),1] -= lateral_period 341 | xyrra_list[:,4] %= 2*pi 342 | xyrra_list[(xyrra_list[:,4]>pi),4] -= 2*pi 343 | 344 | def show_config(self): 345 | grating_period = self.grating_period 346 | lateral_period = self.lateral_period 347 | plt.figure() 348 | plt.xlim(-grating_period / nm, grating_period / nm) 349 | plt.ylim(-lateral_period / nm, lateral_period / nm) 350 | # add lotsa periodic replicas to allow zooming out 351 | for x,y,rx,ry,a in self.get_xyrra_list(replicas=3): 352 | circle = matplotlib.patches.Ellipse((x / nm, y / nm), 353 | 2*rx / nm, 2*ry / nm, 354 | angle=a / degree, 355 | color='k', alpha=0.5) 356 | plt.gcf().gca().add_artist(circle) 357 | rect = matplotlib.patches.Rectangle((-grating_period/2 / nm,-lateral_period/2 / nm), 358 | grating_period / nm, lateral_period / nm, 359 | facecolor='none', linestyle='dashed', 360 | linewidth=2, edgecolor='red') 361 | plt.gcf().gca().add_artist(rect) 362 | plt.gcf().gca().set_aspect('equal') 363 | 364 | def characterize(self, subfolder=None, process=None, 365 | ux_min=None, ux_max=None, uy_min=-0.2, uy_max=0.2, 366 | u_steps=3, wavelength=580*nm, numG=100, convert_to_xy=True, 367 | just_normal=False): 368 | """Calculate grating output as a function of incoming angle with 369 | grating.lua, then store the result in self.data. 370 | Initiate the process separately if you want to run many in parallel 371 | (See GratingCollection.characterize for an example.) 372 | 373 | ampfy, ampfx, ampry, amprx are the outgoing amplitudes for the "x" and 374 | "y" polarizations (see S4conventions for what that means) in each of 375 | the forward ("f") and reflected ("r") directions. 376 | 377 | convert_to_xy switches the two incoming polarizations from 's' and 'p' 378 | (used by grating.lua) to 'x' and 'y' (see S4conventions.py for 379 | definitions). The advantage is that x and y are defined in a smoothly- 380 | varying way across all propagating directions, whereas s and p switch 381 | directions sharply near normal, making interpolation tricky. 382 | 383 | If just_normal is True, the ux,uy inputs are ignored, and instead we 384 | only calculate for normal incidence. We put the data in a format so that 385 | the usual interpolation code still works, in particular we copy the 386 | same data into (ux,uy)=(0.001,0.001), (0.001,-0.001), etc. straddling the 387 | normal direction 388 | """ 389 | if process is None: 390 | if just_normal is True: 391 | ux_min = ux_max = uy_min = uy_max = 0.001 392 | u_steps = 1 393 | else: 394 | if ux_min is None: 395 | target_ux = self.get_angle_in_air(580*nm) 396 | ux_min = max(-0.99, target_ux - 0.2) 397 | if ux_max is None: 398 | target_ux = self.get_angle_in_air(580*nm) 399 | ux_max = min(0.99, target_ux + 0.2) 400 | process = self.run_lua_initiate(subfolder=subfolder, ux_min=ux_min, 401 | ux_max=ux_max, uy_min=uy_min, 402 | uy_max=uy_max, u_steps=u_steps, 403 | wavelength=wavelength, numG=numG) 404 | 405 | output, error = (x.decode() for x in process.communicate()) 406 | if error is not '': 407 | raise ValueError(error) 408 | all_data = [] 409 | for line in output.split('\n'): 410 | split = line.split() 411 | #print(split) 412 | if len(split) > 0: 413 | assert len(split) == 14 414 | all_data.append({'wavelength_in_nm': float(split[0]), 415 | 's_or_p': split[1], 416 | 'ux':float(split[2]), 417 | 'uy':float(split[3]), 418 | 'ox':int(split[4]), 419 | 'oy':int(split[5]), 420 | 'ampfy':float(split[6]) + 1j * float(split[7]), 421 | 'ampfx':float(split[8]) + 1j * float(split[9]), 422 | 'ampry':float(split[10]) + 1j * float(split[11]), 423 | 'amprx':float(split[12]) + 1j * float(split[13])}) 424 | if convert_to_xy is True: 425 | all_data_xy = [] 426 | for ep in (x for x in all_data if x['s_or_p'] == 'p'): 427 | temp = [x for x in all_data if x['wavelength_in_nm']==ep['wavelength_in_nm'] 428 | and (x['ux'],x['uy'])==(ep['ux'],ep['uy']) 429 | and (x['ox'],x['oy'])==(ep['ox'],ep['oy']) 430 | and x['s_or_p']=='s'] 431 | assert len(temp)==1 432 | es = temp[0] 433 | 434 | # Now I have a matching (s,p) pair. Take a linear combination 435 | # to find the results I would get from x and y incident 436 | # polarization. 437 | # Since this only concerns the incident light, use formulas 438 | # with n=1, kx = kx_incident, etc. 439 | k = 2*pi / (es['wavelength_in_nm'] * nm) 440 | kx = k*ep['ux'] 441 | ky = k*ep['uy'] 442 | # at kx==ky==0, s and p are undefined. In practice, -0.0 is 443 | # different than +0.0. It's fraught, best to just forbid it. 444 | assert 0 < kx**2 + ky**2 <= k**2 445 | kz = (k**2 - kx**2 - ky**2)**0.5 446 | 447 | # see S4conventions.py for the following four lines 448 | x_p_coef = kx/(kx**2+ky**2)**0.5 449 | x_s_coef = -ky*k/(kz*(kx**2+ky**2)**0.5) 450 | y_p_coef = -ky/(kx**2+ky**2)**0.5 451 | y_s_coef = -kx*k/(kz*(kx**2+ky**2)**0.5) 452 | 453 | ex = {key:ep[key] for key in ep if key in ('wavelength_in_nm', 454 | 'ux','uy','ox','oy')} 455 | ey = {key:ep[key] for key in ep if key in ('wavelength_in_nm', 456 | 'ux','uy','ox','oy')} 457 | ex['x_or_y'] = 'x' 458 | ey['x_or_y'] = 'y' 459 | for a in ('ampfy','ampfx','ampry','amprx'): 460 | ex[a] = x_p_coef * ep[a] + x_s_coef * es[a] 461 | ey[a] = y_p_coef * ep[a] + y_s_coef * es[a] 462 | all_data_xy.append(ex) 463 | all_data_xy.append(ey) 464 | if just_normal: 465 | assert all(e['ux'] == 0.001 for e in all_data_xy) 466 | assert all(e['uy'] == 0.001 for e in all_data_xy) 467 | for entry in all_data_xy.copy(): 468 | for ux_sign, uy_sign in [(-1,1), (-1,-1), (1,-1)]: 469 | entry2 = entry.copy() 470 | entry2['ux'] *= ux_sign 471 | entry2['uy'] *= uy_sign 472 | all_data_xy.append(entry2) 473 | self.data = all_data_xy 474 | else: 475 | # didn't yet code the just_normal, don't-convert-to-xy case 476 | assert just_normal is False 477 | 478 | self.data = all_data 479 | 480 | def show_characterization(mygrating): 481 | all_data = mygrating.data 482 | ux_list = sorted({x['ux'] for x in all_data}) 483 | uy_list = sorted({x['uy'] for x in all_data}) 484 | my_order = (0,0) 485 | my_pol = 'x' 486 | my_wavelength = 580 487 | 488 | all_data_filtered = [x for x in all_data if x['x_or_y'] == my_pol 489 | and x['ox'] == my_order[0] 490 | and x['oy'] == my_order[1] 491 | and x['wavelength_in_nm'] == my_wavelength] 492 | data = np.zeros(shape=(len(ux_list), len(uy_list)), dtype=complex) + np.nan 493 | 494 | for entry in all_data_filtered: 495 | ix = ux_list.index(entry['ux']) 496 | iy = uy_list.index(entry['uy']) 497 | assert np.isnan(data[ix,iy]) 498 | data[ix,iy] = entry['amprx'] 499 | 500 | plt.figure() 501 | #plt.imshow(np.angle(data).T, interpolation='none', extent=((min(ux_list),max(ux_list),min(uy_list),max(uy_list)))) 502 | plt.imshow(np.abs(data).T, interpolation='none', extent=((min(ux_list),max(ux_list),min(uy_list),max(uy_list)))) 503 | plt.xlabel('ux (x-component of unit vector of incoming light direction)') 504 | plt.ylabel('uy (y-component of unit vector of incoming light direction)') 505 | plt.colorbar() 506 | 507 | 508 | 509 | min_diameter = 100*nm 510 | min_distance = 100*nm 511 | 512 | def sq_distance_mod(x0,y0,x1,y1,x_period,y_period): 513 | """squared distance between two points in a 2d periodic structure""" 514 | dx = min((x0 - x1) % x_period, (x1 - x0) % x_period) 515 | dy = min((y0 - y1) % y_period, (y1 - y0) % y_period) 516 | return dx*dx + dy*dy 517 | 518 | def distance_mod(x0,x1,period): 519 | """1d version - distance between two points in a periodic structure""" 520 | return min((x0 - x1) % period, (x1 - x0) % period) 521 | 522 | def validate(mygrating, print_details=False, similar_to=None, how_similar=None): 523 | """ make sure the structure can be fabricated, doesn't self-intersect, etc. 524 | If similar_to is provided, it's an xyrra_list describing a configuration 525 | that we need to resemble, and how_similar should be a factor like 0.02 526 | meaning the radii etc. should change by less than 2%.""" 527 | xyrra_list = mygrating.xyrra_list 528 | if xyrra_list[:,[2,3]].min() < min_diameter/2: 529 | if print_details: 530 | print('a diameter is too small') 531 | return False 532 | # if xyrra_list[:,3].max() > max_y_diameter/2: 533 | # if print_details: 534 | # print('a y-diameter is too big') 535 | # return False 536 | 537 | # Check that no two shapes are excessively close 538 | # points_list[i][j,k] is the x (if k=0) or y (if k=1) coordinate 539 | # of the j'th point on the border of the i'th shape 540 | points_per_ellipse = 100 541 | num_ellipses = xyrra_list.shape[0] 542 | points_arrays = [] 543 | for i in range(num_ellipses): 544 | points_arrays.append(ellipse_pts(*xyrra_list[i,:], num_points=points_per_ellipse)) 545 | 546 | # first, check each shape against its own periodic replicas, in the 547 | # smaller (y) direction. I assume that shapes will not be big enough to 548 | # approach their periodic replicas 549 | for i in range(num_ellipses): 550 | i_pt_list = points_arrays[i] 551 | j_pt_list = i_pt_list.copy() 552 | j_pt_list[:,1] += mygrating.lateral_period 553 | for i_pt in i_pt_list: 554 | for j_pt in j_pt_list: 555 | if (i_pt[0] - j_pt[0])**2 + (i_pt[1] - j_pt[1])**2 < min_distance**2: 556 | if print_details: 557 | print('too close, between ellipse', i, 'and its periodic replica') 558 | print(i_pt) 559 | print(j_pt) 560 | mygrating.show_config() 561 | plt.plot(i_pt[0]/nm, i_pt[1]/nm, 'r.') 562 | plt.plot(j_pt[0]/nm, j_pt[1]/nm, 'r.') 563 | return False 564 | 565 | for i in range(1,num_ellipses): 566 | i_pt_list = points_arrays[i] 567 | for j in range(i): 568 | j_pt_list = points_arrays[j] 569 | for i_pt in i_pt_list: 570 | for j_pt in j_pt_list: 571 | if sq_distance_mod(i_pt[0], i_pt[1], j_pt[0], j_pt[1], 572 | mygrating.grating_period, mygrating.lateral_period) < min_distance**2: 573 | if print_details: 574 | print('too close, between ellipse', j, 'and', i) 575 | print(i_pt) 576 | print(j_pt) 577 | mygrating.show_config() 578 | plt.plot(i_pt[0]/nm, i_pt[1]/nm, 'r.') 579 | plt.plot(j_pt[0]/nm, j_pt[1]/nm, 'r.') 580 | return False 581 | if similar_to is not None: 582 | for i in range(num_ellipses): 583 | if max(abs(xyrra_list[i, 2:4] - similar_to[i, 2:4]) / similar_to[i, 2:4]) > how_similar: 584 | if print_details: 585 | print('A radius of ellipse', i, 'changed too much') 586 | return False 587 | if distance_mod(xyrra_list[i,0], similar_to[i,0], mygrating.grating_period) > how_similar * mygrating.grating_period: 588 | if print_details: 589 | print('x-coordinate of ellipse', i, 'changed too much') 590 | return False 591 | if distance_mod(xyrra_list[i,1], similar_to[i,1], mygrating.lateral_period) > how_similar * mygrating.lateral_period: 592 | if print_details: 593 | print('y-coordinate of ellipse', i, 'changed too much') 594 | return False 595 | if distance_mod(xyrra_list[i,4], similar_to[i,4], 2*pi) > how_similar * (2*pi): 596 | if print_details: 597 | print('rotation of ellipse', i, 'changed too much') 598 | return False 599 | return True 600 | 601 | def resize(oldgrating, newgrating_shell): 602 | """the vary_angle() routine gradually changes the periodicity of the 603 | grating. As a starting condition, the simplest thing would be to use the 604 | old xyrra_list with the new periodicity. But it's possible in this case 605 | for the grating to fail validate(). 606 | 607 | For the cylindrical lens, in vary_angle(), lateral_period is fixed and 608 | grating_period increases, so there's no possible problem. 609 | 610 | For the round lens, in vary_angle(), lateral_period is increasing while 611 | grating_period is decreasing. The latter could cause a problem. But usually 612 | there is a big gap somewhere along the x dimension, so we can shrink that 613 | gap a bit. 614 | 615 | oldgrating is the previous grating, which passed validation. 616 | newgrating_shell is the new grating with no xyrra_list yet. We will fill 617 | it out and return a fresh grating""" 618 | oldgrating = oldgrating.copy() 619 | oldgrating.standardize() 620 | g = newgrating_shell.copy() 621 | g.xyrra_list = oldgrating.xyrra_list.copy() 622 | if validate(g) is True: 623 | return g 624 | 625 | old_grating_period = oldgrating.grating_period 626 | new_grating_period = g.grating_period 627 | 628 | assert new_grating_period < old_grating_period 629 | assert g.lateral_period >= oldgrating.lateral_period 630 | 631 | # look for places to try cutting horizontal space, i.e. x-coordinates with 632 | # a lot of clearance to the nearest pillar 633 | try_cutting_list = np.linspace(-old_grating_period/2, old_grating_period/2, 634 | num=100, endpoint=False) 635 | clearance_list = np.zeros_like(try_cutting_list) + np.inf 636 | for xc,yc,rx,ry,a in oldgrating.xyrra_list: 637 | for x,y in ellipse_pts(xc,yc,rx,ry,a,num_points=80): 638 | for i, xnow in enumerate(try_cutting_list): 639 | clearance_list[i] = min(clearance_list[i], 640 | distance_mod(xnow, x, old_grating_period)) 641 | x_to_cut_at = try_cutting_list[np.argmax(clearance_list)] 642 | 643 | for i in range(g.xyrra_list.shape[0]): 644 | if g.xyrra_list[i,0] > x_to_cut_at: 645 | g.xyrra_list[i,0] -= (old_grating_period - new_grating_period) 646 | 647 | assert validate(g, print_details=True) 648 | return g 649 | 650 | 651 | 652 | def correct_imshow_extent(array, min_px_center_x, max_px_center_x, 653 | min_px_center_y, max_px_center_y): 654 | """imshow extent specifies the left side of the leftmost pixel etc. I want 655 | to instead say what the coordinate is at the center of the leftmost pixel etc.""" 656 | nx = array.shape[1] 657 | ny = array.shape[0] 658 | px_extent_x = (max_px_center_x - min_px_center_x) / (nx-1) 659 | px_extent_y = (max_px_center_y - min_px_center_y) / (ny-1) 660 | return [min_px_center_x - px_extent_x/2, 661 | max_px_center_x + px_extent_x/2, 662 | min_px_center_y - px_extent_y/2, 663 | max_px_center_y + px_extent_y/2] 664 | 665 | 666 | def ellipse_pts(x_center, y_center, r_x, r_y, angle, num_points=80): 667 | """return a list of (x,y) coordinates of points on an ellipse, in CCW order""" 668 | xy_list = np.empty(shape=(num_points,2)) 669 | theta_list = np.linspace(0,2*pi,num=num_points, endpoint=False) 670 | for i,theta in enumerate(theta_list): 671 | dx0 = r_x * math.cos(theta) 672 | dy0 = r_y * math.sin(theta) 673 | xy_list[i,0] = x_center + dx0 * math.cos(angle) - dy0 * math.sin(angle) 674 | xy_list[i,1] = y_center + dx0 * math.sin(angle) + dy0 * math.cos(angle) 675 | if False: 676 | # test 677 | plt.figure() 678 | plt.plot(x_center,y_center,'r.') 679 | for x,y in xy_list: 680 | plt.plot(x,y,'k.') 681 | plt.gca().set_aspect('equal') 682 | return xy_list 683 | 684 | 685 | def optimize(mygrating_start, target_wavelength, similar_to=None, how_similar=None, 686 | subfolder=None, numG=50): 687 | """optimize by varying parameters one after the other. 688 | If similar_to is provided, it's an xyrra_list describing a configuration 689 | that we need to resemble, and how_similar should be a factor like 0.02 690 | meaning the radii etc. should change by less than 2%. 691 | 692 | Target_wavelength is not what the simulation is run at (which is determined 693 | in grating.lua), but rather for determining what angle the light is coming 694 | in at (which is the same for all wavelengths, in a collimator). 695 | 696 | Return optimized Grating object (input object is not altered).""" 697 | assert validate(mygrating_start, print_details=True, 698 | similar_to=similar_to, how_similar=how_similar) 699 | mygrating = mygrating_start.copy() 700 | xyrra_list = mygrating.xyrra_list 701 | fom_now = mygrating.run_lua(subfolder=subfolder, 702 | target_wavelength=target_wavelength, numG=numG) 703 | print('fom now...', fom_now, flush=True) 704 | found_optimum = False 705 | things_to_try_changing = [(i,j) for i in range(xyrra_list.shape[0]) 706 | for j in range(xyrra_list.shape[1])] 707 | while found_optimum is False: 708 | random.shuffle(things_to_try_changing) 709 | found_optimum = True 710 | for index in things_to_try_changing: 711 | # dont_bother... is if FOM improves by decreasing a parameter, 712 | # there's no point in trying to increase it right afterwards 713 | dont_bother_trying_opposite_change = False 714 | if index[1] == 4: 715 | changes = [-.3*degree, .3*degree] 716 | else: 717 | changes = [-1*nm, 1*nm] 718 | for change in changes: 719 | if dont_bother_trying_opposite_change is True: 720 | continue 721 | for _ in range(10): 722 | # when you find a good change, keep applying it over and over 723 | # until it stops working. This speeds up the optimization. 724 | # But I limited to 10 repeats so that we find the local 725 | # optimum near the initial conditions. 726 | xyrra_list[index] += change 727 | if not validate(mygrating, similar_to=similar_to, how_similar=how_similar): 728 | xyrra_list[index] -= change 729 | break 730 | fom_new = mygrating.run_lua(subfolder=subfolder, 731 | target_wavelength=target_wavelength, 732 | numG=numG) 733 | if fom_new < fom_now: 734 | xyrra_list[index] -= change 735 | break 736 | else: 737 | mygrating.standardize() 738 | assert validate(mygrating, similar_to=similar_to, how_similar=how_similar) 739 | print('#New record! ', fom_new) 740 | print('mygrating='+repr(mygrating), flush=True) 741 | print('', flush=True) 742 | fom_now = fom_new 743 | found_optimum = False 744 | dont_bother_trying_opposite_change = True 745 | return mygrating 746 | 747 | def optimize2(mygrating_start, target_wavelength, attempts=inf, similar_to=None, 748 | how_similar=None, subfolder=None, numG=50): 749 | """vary parameters randomly. 750 | 751 | Target_wavelength is not what the simulation is run at (which is determined 752 | in grating.lua), but rather for determining what angle the light is coming 753 | in at (which is the same for all wavelengths, in a collimator). 754 | 755 | Return optimized Grating object (input object is not altered).""" 756 | assert validate(mygrating_start, print_details=True, 757 | similar_to=similar_to, how_similar=how_similar) 758 | mygrating = mygrating_start.copy() 759 | xyrra_list = mygrating.xyrra_list 760 | fom_now = mygrating.run_lua(subfolder=subfolder, 761 | target_wavelength=target_wavelength, numG=numG) 762 | print('fom now...', fom_now, flush=True) 763 | max_change_array = np.empty_like(xyrra_list) 764 | max_change_array[:, 0:4] = 1*nm 765 | max_change_array[:,4] = 0.1*degree 766 | max_change_array /= xyrra_list.size 767 | attempts_so_far = 0 768 | while attempts_so_far < attempts: 769 | attempts_so_far += 1 770 | xyrra_list_change = max_change_array*(2*np.random.random(size=xyrra_list.shape)-1) 771 | for _ in range(10): 772 | # when you find a good change, keep applying it over and over 773 | # until it stops working. This speeds up the optimization. 774 | # But I limited to 10 repeats so that we find the local 775 | # optimum near the initial conditions. 776 | xyrra_list += xyrra_list_change 777 | if not validate(mygrating, similar_to=similar_to, how_similar=how_similar): 778 | xyrra_list -= xyrra_list_change 779 | break 780 | fom_new = mygrating.run_lua(subfolder=subfolder, 781 | target_wavelength=target_wavelength, 782 | numG=numG) 783 | if fom_new < fom_now: 784 | #print('lower fom', fom_new) 785 | xyrra_list -=xyrra_list_change 786 | break 787 | else: 788 | mygrating.standardize() 789 | assert validate(mygrating, similar_to=similar_to, 790 | how_similar=how_similar, print_details=True) 791 | print('#New record! ', fom_new) 792 | print('mygrating='+repr(mygrating), flush=True) 793 | print('', flush=True) 794 | fom_now = fom_new 795 | return mygrating 796 | 797 | def plot_eps(): 798 | a = np.genfromtxt(os.path.join(cwd_for_S4(), 'grating_eps.txt')) 799 | xs = np.unique(a[:,0]) 800 | ys = np.unique(a[:,1]) 801 | 802 | eps_matrix = np.zeros(shape=(len(xs),len(ys)),dtype=complex) 803 | for row in a: 804 | ix = np.nonzero(xs == row[0])[0] 805 | iy = np.nonzero(ys == row[1])[0] 806 | eps_matrix[ix,iy] = row[3] + 1j * row[4] 807 | 808 | plt.figure() 809 | plt.imshow((eps_matrix.real.T)**0.5, origin='lower', aspect='equal', extent=(min(xs), max(xs), min(ys), max(ys))) 810 | plt.title('index') 811 | plt.colorbar() 812 | return eps_matrix 813 | 814 | def stretch_pattern(xyrra_list_start, x_scale, y_scale): 815 | xyrra_list = xyrra_list_start.copy() 816 | xyrra_list[:,[0,2]] *= x_scale 817 | xyrra_list[:,[1,3]] *= y_scale 818 | return xyrra_list 819 | 820 | def vary_angle(start_grating=None, end_angle=None, lens_type=None, 821 | target_wavelength=None, start_grating_collection=None, 822 | subfolder=None, numG=50): 823 | """Repeatedly increase the grating_period by little steps, then tweak the 824 | pattern to have high efficiency. But don't change it too much so that it 825 | stays almost-periodic. lens_type should be 'cyl' or 'round'. 826 | 827 | For cyl lens, we work our way towards the center of the lens, increasing 828 | the grating_period. For round_lens, it's the reverse, we decrease 829 | grating_period while increasing lateral_period. That makes it most easy to 830 | keep xyrra_list the same to start the next step without violating the 831 | validate() geometric constraints. (lateral_period typically changes 832 | more quickly than grating_period). 833 | 834 | Don't optimize the starting pattern, assume it's already done. 835 | 836 | Subfolder runs the simulations in a different directory, so that you can 837 | have multiple iPython interpreters running this function simultaneously""" 838 | 839 | #Exactly one of these parameters must be provided 840 | assert (start_grating_collection is None) != (start_grating is None 841 | and target_wavelength is None) 842 | 843 | ### Initialize all_gratings - the grating collection we are making 844 | if start_grating_collection is not None: 845 | all_gratings = start_grating_collection 846 | else: 847 | if lens_type == 'cyl': 848 | all_gratings = GratingCollection(target_wavelength=target_wavelength, 849 | lateral_period=start_grating.lateral_period, 850 | grating_list=[start_grating], 851 | lens_type='cyl') 852 | else: 853 | assert lens_type == 'round' 854 | # for round lenses, the lateral_period parameter is redefined as 855 | # "lateral_period at a certain point / tan(angle_in_air) at that point" 856 | angle_in_air = start_grating.get_angle_in_air(target_wavelength=target_wavelength) 857 | lateral_period = start_grating.lateral_period / math.tan(angle_in_air) 858 | all_gratings = GratingCollection(target_wavelength=target_wavelength, 859 | lateral_period=lateral_period, 860 | grating_list=[start_grating], 861 | lens_type='round') 862 | 863 | if all_gratings.lens_type == 'cyl': 864 | # change the grating_period by this fraction each step 865 | change_each_step = 1.01 866 | # how much we allow the pattern to change each step 867 | similarity_each_step = 0.03 868 | else: 869 | # change the lateral_period by this fraction each step 870 | change_each_step = 1.01 871 | similarity_each_step = 0.03 872 | 873 | 874 | 875 | while True: 876 | print('grating collection so far:') 877 | print(repr(all_gratings)) 878 | 879 | # grating_list is sorted from lens-outside to lens-center 880 | # so for cylindrical lens, we put new entries after the last entry 881 | # whereas for round lens, we put new entries before the first entry 882 | if all_gratings.lens_type == 'cyl': 883 | grating_prev = all_gratings.grating_list[-1] 884 | else: 885 | grating_prev = all_gratings.grating_list[0] 886 | 887 | if all_gratings.lens_type == 'cyl': 888 | grating_new_start = all_gratings.get_one( 889 | grating_period=grating_prev.grating_period * change_each_step) 890 | else: 891 | grating_new_start = all_gratings.get_one( 892 | lateral_period=grating_prev.lateral_period * change_each_step) 893 | angle_in_air = grating_new_start.get_angle_in_air( 894 | target_wavelength=all_gratings.target_wavelength) 895 | if angle_in_air < end_angle and all_gratings.lens_type == 'cyl': 896 | break 897 | if angle_in_air > end_angle and all_gratings.lens_type == 'round': 898 | break 899 | 900 | #xyrra_list_prev = grating_prev.xyrra_list.copy() 901 | print('Optimizing for angle_in_air = ', angle_in_air/degree, 'degree') 902 | 903 | grating_new_start = resize(grating_prev, grating_new_start) 904 | 905 | grating_new = optimize(grating_new_start, 906 | target_wavelength=all_gratings.target_wavelength, 907 | similar_to=grating_new_start.xyrra_list, 908 | how_similar=similarity_each_step, subfolder=subfolder, 909 | numG=numG) 910 | grating_new = optimize2(grating_new, attempts=200, 911 | target_wavelength=all_gratings.target_wavelength, 912 | similar_to=grating_new_start.xyrra_list, 913 | how_similar=similarity_each_step, subfolder=subfolder, 914 | numG=numG) 915 | 916 | all_gratings.add_one(grating_new) 917 | 918 | return all_gratings 919 | 920 | class GratingCollection: 921 | """A smoothly-varying collection of Grating objects for different angles. 922 | Use lens_type='cyl' for cylindrical lens, i.e. lens that focuses only in 923 | one of the two lateral directions. Use lens_type='round' for normal lenses. 924 | 925 | For cylindrical lens design: lateral_period is constant 926 | For round lens design: lateral_period is short for 927 | "lateral_period over tan(angle_in_air)", which is constant for the whole 928 | collection. This does NOT take into account the fact that there has to be 929 | an integer number of repetitions going around the circle, because that's a 930 | negligible correction except for tiny tiny lenses. 931 | 932 | target_wavelength is critical for round lenses because it determines the 933 | relation between lateral_period and grating_period. For cylindrical lenses, 934 | it doesn't really matter, but I'm requiring it anyway for convenience and 935 | simplicity. 936 | """ 937 | def __init__(self, target_wavelength, lateral_period, 938 | lens_type='cyl', grating_list=None): 939 | """initialize a GratingCollection. grating_list (optional) is a python 940 | list of Grating objects""" 941 | self.target_wavelength = target_wavelength 942 | self.lateral_period = lateral_period 943 | self.target_kvac = 2*pi / target_wavelength 944 | self.lens_type = lens_type 945 | 946 | assert self.lens_type in ('cyl', 'round') 947 | 948 | if grating_list is None: 949 | self.grating_list = [] 950 | else: 951 | self.grating_list = grating_list 952 | self.sort_grating_list() 953 | self.check_consistency() 954 | 955 | def check_consistency(self): 956 | """just make sure that all the gratings have consistent cyl_height, 957 | refractive index, lateral_period, etc.""" 958 | assert len({g.cyl_height for g in self.grating_list}) <= 1 959 | assert len({g.n_glass for g in self.grating_list}) <= 1 960 | assert len({g.n_tio2 for g in self.grating_list}) <= 1 961 | if self.lens_type == 'cyl': 962 | assert all(self.lateral_period == g.lateral_period 963 | for g in self.grating_list) 964 | else: 965 | assert self.lens_type == 'round' 966 | wl = self.target_wavelength 967 | g = [g.lateral_period / math.tan(g.get_angle_in_air(target_wavelength=wl)) 968 | for g in self.grating_list] 969 | assert (max(g) - min(g)) < 1e-7 * max(g) 970 | 971 | def sort_grating_list(self): 972 | self.grating_list.sort(key=lambda x:x.grating_period) 973 | 974 | def add_one(self, new_grating): 975 | """add a grating to this collection. 976 | Input EITHER the angle_in_air OR the grating_period""" 977 | self.grating_list.append(new_grating) 978 | self.grating_list.sort(key=lambda x:x.grating_period) 979 | self.check_consistency() 980 | 981 | def get_one(self, angle_in_air=None, grating_period=None, lateral_period=None): 982 | """get a sim_setup object from this collection. 983 | Input EITHER the angle_in_air OR the grating_period, 984 | OR (round lens only) the local lateral_period (not lateral_period/tan angle) 985 | Can be outside the range where we have xyrra_lists, in which case we 986 | return a Grating object with blank xyrra_list.""" 987 | # first, calculate grating_period (if it wasn't supplied) 988 | if grating_period is not None: 989 | assert angle_in_air is None and lateral_period is None 990 | elif angle_in_air is not None: 991 | assert lateral_period is None 992 | grating_period = self.target_wavelength / math.sin(angle_in_air) 993 | else: 994 | # a lateral_period was supplied 995 | assert self.lens_type == 'round' 996 | # self.lateral_period is short for lateral_period / tan(angle_in_air) 997 | angle_in_air = math.atan(lateral_period / self.lateral_period) 998 | grating_period = self.target_wavelength / math.sin(angle_in_air) 999 | 1000 | # calculate lateral_period 1001 | if self.lens_type == 'cyl': 1002 | lateral_period = self.lateral_period 1003 | else: 1004 | angle_in_air = math.asin(self.target_wavelength / grating_period) 1005 | lateral_period = self.lateral_period * math.tan(angle_in_air) 1006 | 1007 | # Find xyrra_list 1008 | self.sort_grating_list() 1009 | 1010 | # if grating_period is within the bounds of the list, or 1% beyond, 1011 | # then interpolate to find xyrra_list. (The 1% beyond is for 1012 | # convenience, in case you have GratingCollections that don't *quite* 1013 | # overlap.) Otherwise, return an empty xyrra_list 1014 | if (grating_period < self.grating_list[0].grating_period * 0.99 1015 | or grating_period > self.grating_list[-1].grating_period * 1.01): 1016 | xyrra_list_in_nm_deg = None 1017 | elif grating_period > self.grating_list[-1].grating_period: 1018 | xyrra_list_in_nm_deg = self.grating_list[-1].xyrra_list_in_nm_deg 1019 | elif grating_period < self.grating_list[0].grating_period: 1020 | xyrra_list_in_nm_deg = self.grating_list[0].xyrra_list_in_nm_deg 1021 | elif any(g.grating_period == grating_period for g in self.grating_list): 1022 | # perfect match is already in the collection 1023 | i = [g.grating_period for g in self.grating_list].index(grating_period) 1024 | xyrra_list_in_nm_deg = self.grating_list[i].xyrra_list_in_nm_deg 1025 | else: 1026 | # interpolate. We're looking between entry i-1 and i. 1027 | i = next(j for j,g in enumerate(self.grating_list) 1028 | if g.grating_period > grating_period) 1029 | grating_before = self.grating_list[i-1] 1030 | period_before = grating_before.grating_period 1031 | grating_after = self.grating_list[i] 1032 | period_after = grating_after.grating_period 1033 | assert (period_before < grating_period < period_after) 1034 | after_weight = (grating_period - period_before) / (period_after - period_before) 1035 | before_weight = (period_after - grating_period) / (period_after - period_before) 1036 | assert (0 < before_weight < 1) and (0 < after_weight < 1) 1037 | assert before_weight + after_weight == 1 1038 | 1039 | xyrra_list_in_nm_deg = (before_weight * grating_before.xyrra_list_in_nm_deg 1040 | + after_weight * grating_after.xyrra_list_in_nm_deg) 1041 | 1042 | return Grating(lateral_period=lateral_period, 1043 | cyl_height=self.grating_list[0].cyl_height, 1044 | grating_period=grating_period, 1045 | n_glass=self.grating_list[0].n_glass, 1046 | n_tio2=self.grating_list[0].n_tio2, 1047 | xyrra_list_in_nm_deg=xyrra_list_in_nm_deg) 1048 | 1049 | def get_innermost(self): 1050 | """return the Grating object for the closest-to-lens-center part of 1051 | this GratingCollection""" 1052 | return self.grating_list[-1] 1053 | 1054 | def get_outermost(self): 1055 | """return the SimSetup object for the closest-to-lens-center part of 1056 | this GratingCollection""" 1057 | return self.grating_list[0] 1058 | 1059 | def show_efficiencies(self, numG=100): 1060 | """Calculate the efficiencies of each grating in the collection""" 1061 | angles_efficiencies_list = [] 1062 | process_list = [] 1063 | # Calculate efficiencies in parallel by spawning all the processes 1064 | # before reading any of them 1065 | subfolder=random_subfolder_name() 1066 | for i,g in enumerate(self.grating_list): 1067 | process_list.append(g.run_lua_initiate(target_wavelength=self.target_wavelength, 1068 | subfolder=subfolder + str(i), 1069 | numG=numG)) 1070 | for i,g in enumerate(self.grating_list): 1071 | eff = g.run_lua_getresult(process_list[i]) 1072 | remove_subfolder(subfolder + str(i)) 1073 | angle = g.get_angle_in_air(self.target_wavelength) 1074 | print('angle_in_air:', angle/degree, 'deg, effic:', eff) 1075 | angles_efficiencies_list.append((angle, eff)) 1076 | 1077 | plt.figure() 1078 | plt.plot([x[0] / degree for x in angles_efficiencies_list], 1079 | [x[1] for x in angles_efficiencies_list]) 1080 | return angles_efficiencies_list 1081 | 1082 | def __repr__(self): 1083 | """this is important - after we spend hours finding a nice 1084 | GratingCollection, let's say "mycollection", we can just type 1085 | "mycollection" or "repr(mycollection)" into IPython and copy the 1086 | resulting text. That's the code to re-create that grating collection 1087 | immediately, without re-calculating it.""" 1088 | return ('GratingCollection(' 1089 | + 'target_wavelength=' + repr(self.target_wavelength/nm) + '*nm' 1090 | + ', lateral_period=' + repr(self.lateral_period/nm) + '*nm' 1091 | + ', lens_type=' + repr(self.lens_type) 1092 | + ', grating_list= ' + repr(self.grating_list) 1093 | + ')') 1094 | 1095 | def show_graphs(self, with_efficiencies=False, 1096 | anim_filename='grating_collection_anim.gif', numG=100): 1097 | max_grating_period = max(g.grating_period for g in self.grating_list) 1098 | max_lateral_period = max(g.lateral_period for g in self.grating_list) 1099 | filename_list = [] 1100 | 1101 | # Calculate efficiencies in parallel by spawning all the processes 1102 | # before reading any of them 1103 | if with_efficiencies is True: 1104 | subfolder=random_subfolder_name() 1105 | process_list = [] 1106 | for i,g in enumerate(self.grating_list[::-1]): 1107 | process_list.append(g.run_lua_initiate(target_wavelength=self.target_wavelength, 1108 | subfolder=subfolder+str(i), numG=numG)) 1109 | for i,g in enumerate(self.grating_list[::-1]): 1110 | g.show_config() 1111 | plt.xlim(-max_grating_period/nm, max_grating_period/nm) 1112 | plt.ylim(-max_lateral_period/nm, max_lateral_period/nm) 1113 | filename_list.append('grating_collection' + str(i) + '.png') 1114 | angle = g.get_angle_in_air(self.target_wavelength) / degree 1115 | if with_efficiencies is True: 1116 | eff = g.run_lua_getresult(process_list[i]) 1117 | remove_subfolder(subfolder+str(i)) 1118 | plt.title('From angle: {:.1f}°, effic={:.2%}'.format(angle, eff)) 1119 | else: 1120 | plt.title('From angle: {:.1f}°'.format(angle)) 1121 | plt.savefig(filename_list[-1]) 1122 | plt.close() 1123 | seconds_per_frame = 0.3 1124 | frame_delay = str(int(seconds_per_frame * 100)) 1125 | # Use the "convert" command (part of ImageMagick) to build the animation 1126 | command_list = ['convert', '-delay', frame_delay, '-loop', '0'] + filename_list + [anim_filename] 1127 | if True: 1128 | #### WINDOWS #### 1129 | subprocess.call(command_list, shell=True) 1130 | else: 1131 | #### LINUX #### 1132 | subprocess.call(command_list) 1133 | if True: 1134 | for filename in filename_list: 1135 | os.remove(filename) 1136 | 1137 | def export_to_lumerical(self, angle_in_air=None, grating_period=None, lateral_period=None): 1138 | """Set up for running grating_lumerical.lsf for either one grating in 1139 | this grating collection, or all of them. 1140 | If no argument is supplied, run all of them. Or, input EITHER the 1141 | angle_in_air OR the grating_period, OR (round lens only) the local 1142 | lateral_period (not lateral_period/tan angle) to run just one""" 1143 | if any(x is not None for x in (angle_in_air, grating_period, lateral_period)): 1144 | mygrating = self.get_one(angle_in_air=angle_in_air, 1145 | grating_period=grating_period, 1146 | lateral_period=lateral_period) 1147 | mygrating.run_lumerical() 1148 | return 1149 | i = 0 1150 | for g in self.grating_list: 1151 | i += 1 1152 | angle_in_air = g.get_angle_in_air(self.target_wavelength) 1153 | g.write(angle_in_air=angle_in_air, index=0, replicas=True) 1154 | # delete the one after that so that lumerical knows to quit 1155 | if os.path.isfile(xyrra_filename(index=i+1)): 1156 | os.remove(xyrra_filename(index=i+1)) 1157 | if os.path.isfile(setup_filename(index=i+1)): 1158 | os.remove(setup_filename(index=i+1)) 1159 | 1160 | def characterize(self, wavelength, numG=100, u_steps=5, just_normal=False): 1161 | """run g.characterize() for each g in the GratingCollection. This stores 1162 | far-field amplitude information in the g object""" 1163 | if just_normal: 1164 | ux_min=ux_max=uy_min=uy_max=0.001 1165 | u_steps=1 1166 | else: 1167 | target_ux_min = self.get_innermost().get_angle_in_air(self.target_wavelength) 1168 | target_ux_max = self.get_outermost().get_angle_in_air(self.target_wavelength) 1169 | ux_min = max(-0.99, target_ux_min - 0.25) 1170 | ux_max = min(0.99, target_ux_max + 0.25) 1171 | uy_min= -0.2 1172 | uy_max=0.2 1173 | subfolder=random_subfolder_name() 1174 | process_list = [] 1175 | for i,g in enumerate(self.grating_list): 1176 | process = g.run_lua_initiate(subfolder=subfolder + str(i), 1177 | ux_min=ux_min, ux_max=ux_max, 1178 | uy_min=uy_min, uy_max=uy_max, 1179 | u_steps=u_steps, wavelength=wavelength, 1180 | numG=numG) 1181 | process_list.append(process) 1182 | for i,g in enumerate(self.grating_list): 1183 | g.characterize(process = process_list[i], just_normal=just_normal) 1184 | remove_subfolder(subfolder+str(i)) 1185 | 1186 | def build_interpolators(self): 1187 | """after running self.characterize(), this creates interpolator objects 1188 | 1189 | If f = self.interpolators[(580,(1,2),'x','amprx')] 1190 | then f([[0.3,0.1,1000*nm], [0.4,0.2,1010*nm]]) is [X,Y] where X is 1191 | the amprx for light coming at (ux,uy=(0.3,0.1)) onto a 1000nm grating 1192 | at 580nm, x-polarization (see S4conventions.py), (1,2) diffraction 1193 | order, and Y is likewise for the next entry""" 1194 | self.interpolators = {} 1195 | ux_list = sorted({e['ux'] for g in self.grating_list for e in g.data}) 1196 | uy_list = sorted({e['uy'] for g in self.grating_list for e in g.data}) 1197 | grating_period_list = sorted({g.grating_period for g in self.grating_list}) 1198 | lookup_table = {(round(e['wavelength_in_nm']),e['ox'],e['oy'],e['x_or_y'],e['ux'],e['uy'],g.grating_period): e 1199 | for g in self.grating_list for e in g.data} 1200 | 1201 | for wavelength_in_nm in {round(e['wavelength_in_nm']) for g in self.grating_list for e in g.data}: 1202 | for (ox,oy) in {(e['ox'], e['oy']) for g in self.grating_list for e in g.data}: 1203 | for x_or_y in ('x','y'): 1204 | #for amp in ('ampfy','ampfx','ampry','amprx'): 1205 | for amp in ('ampfy','ampfx'): 1206 | # want grid_data[i,j,k] = amp(ux_list[i], uy_list[j], grating_period_list[k]) 1207 | grid_data = np.zeros((len(ux_list), len(uy_list), len(grating_period_list)), 1208 | dtype=complex) 1209 | for i,ux in enumerate(ux_list): 1210 | for j,uy in enumerate(uy_list): 1211 | for k, grating_period in enumerate(grating_period_list): 1212 | entry=lookup_table.get((wavelength_in_nm,ox,oy,x_or_y,ux,uy,grating_period)) 1213 | if entry is not None: 1214 | grid_data[i,j,k] = entry[amp] 1215 | # vary_angle stops a bit short of the target angle 1216 | # (TODO - fix that! Then delete this little workaround.) 1217 | # ...therefore we allow grating periods slightly outside 1218 | # the range by using the innermost or outermost grating. 1219 | grid_data_extended = np.zeros((len(ux_list),len(uy_list),2+len(grating_period_list)), 1220 | dtype=complex) 1221 | grid_data_extended[:, :, 1:-1] = grid_data 1222 | grid_data_extended[:, :, 0] = grid_data[:, :, 0] 1223 | grid_data_extended[:, :, -1] = grid_data[:, :, -1] 1224 | grating_period_list_extended = np.hstack((0.99 * min(grating_period_list), 1225 | grating_period_list, 1226 | 1.01 * max(grating_period_list))) 1227 | interp_function = RegularGridInterpolator((ux_list,uy_list,grating_period_list_extended), 1228 | grid_data_extended) 1229 | self.interpolators[(wavelength_in_nm, (ox,oy), x_or_y, amp)] = interp_function 1230 | self.interpolator_bounds = (min(ux_list), max(ux_list), min(uy_list), 1231 | max(uy_list), min(grating_period_list_extended), 1232 | max(grating_period_list_extended)) 1233 | 1234 | #def read_lumerical_batch_analysis(): 1235 | # i = 0 1236 | # while os.path.isfile(xyrra_filename(index=i)): 1237 | # with open(xyrra_filename(index=i), 'r') as f: 1238 | # pass 1239 | # # TO DO 1240 | # i += 1 1241 | 1242 | 1243 | def plot_round_lateral_period(f, reps_around_circumference, target_wavelength=580*nm): 1244 | """quick investigation of how lateral_period and grating_period vary""" 1245 | distances_from_center = np.linspace(100*nm, f*5, num=1000) 1246 | angles = np.array([math.atan(d/f) for d in distances_from_center]) 1247 | lateral_periods = np.array([2*pi*d / reps_around_circumference for d in distances_from_center]) 1248 | grating_periods = np.array([target_wavelength / math.sin(angle) for angle in angles]) 1249 | 1250 | plt.figure() 1251 | plt.plot(lateral_periods / nm, grating_periods / nm) 1252 | plt.xlabel('lateral period (nm)') 1253 | plt.ylabel('grating period (nm)') 1254 | plt.xlim(0,800) 1255 | plt.ylim(0,2000) 1256 | plt.grid() 1257 | 1258 | plt.figure() 1259 | plt.plot(lateral_periods / nm, angles / degree) 1260 | plt.xlabel('lateral period (nm)') 1261 | plt.ylabel('angle (degree)') 1262 | plt.xlim(0,800) 1263 | plt.grid() 1264 | 1265 | plt.figure() 1266 | plt.plot(angles[0:-1] / degree, [(lateral_periods[i+1] / lateral_periods[i] - 1) / (grating_periods[i] / grating_periods[i+1] - 1) for i in range(len(angles)-1)]) 1267 | plt.plot(angles[0:-1] / degree, [1 for i in range(len(angles)-1)]) 1268 | plt.xlabel('angle (degree)') 1269 | plt.ylabel('(How fast lateral_period changes)/(How fast grating_period changes)') 1270 | plt.grid() 1271 | #plot_round_lateral_period(150*um, 3427) 1272 | 1273 | 1274 | def n_glass(wavelength_in_nm): 1275 | """for better data see refractive_index.py .. but this is what I'm using in 1276 | lumerical and lua, and I want to be consistent sometimes""" 1277 | data = {450: 1.466, 1278 | 500: 1.462, 1279 | 525: 1.461, 1280 | 550: 1.46, 1281 | 575: 1.459, 1282 | 580: 1.459, 1283 | 600: 1.458, 1284 | 625: 1.457, 1285 | 650: 1.457} 1286 | if wavelength_in_nm not in data: 1287 | raise ValueError('bad wavelength'+repr(wavelength_in_nm)) 1288 | return data[wavelength_in_nm] 1289 | --------------------------------------------------------------------------------