├── README.md └── code ├── README.md ├── animated_opt.gif ├── asp.jl └── inverse_design_WFOV_metalens.jl /README.md: -------------------------------------------------------------------------------- 1 | # metalens_inverse_design 2 | 3 | This repository contains code for the inverse design of a metalens with a wide field of view (FOV). Each file is annotated with detailed comments for ease of understanding. 4 | 5 | * `inverse_design_WFOV_metalens.jl`: Build, simulate and optimize a metalens 6 | * `asp.jl`: Implement angular spectrum propagation for field transmission from the metalens exit surface to the focal plane 7 | 8 | 9 | To conduct efficient optimizations, follow these steps: 10 | * Install [MESTI.jl](https://github.com/complexphoton/MESTI.jl), an open-source software for full-wave electromagnetic simulations. Prior to this, install the parallel version of [MUMPS](https://mumps-solver.org/index.php) for efficient matrix handling. For other required packages, please refer to the comments of `inverse_design_WFOV_metalens.jl`. 11 | * Install [NLopt.jl](https://github.com/JuliaOpt/NLopt.jl) for access to well-developed optimization algorithms. 12 | * Run `inverse_design_WFOV_metalens.jl` 13 | 14 | 15 | An animation demonstrating the evolution of both the metalens and its focusing performance is shown below. 16 | 17 | 18 | See the paper [High-efficiency high-numerical-aperture metalens designed by maximizing the efficiency limit](https://opg.optica.org/optica/fulltext.cfm?uri=optica-11-4-454&id=548448) for more details. 19 | -------------------------------------------------------------------------------- /code/README.md: -------------------------------------------------------------------------------- 1 | Here, we inverse design a metalens with wide field of view (FOV), by changing the permittivity of each design pixel inside the metalens. 2 | Its focusing performance is improved by maximizing the product of transmission efficiency and Strehl ratio for each incident angle within the FOV of interest. 3 | -------------------------------------------------------------------------------- /code/animated_opt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexphoton/metalens_inverse_design/5702b0586fb4daea8bcc2e54df95a9e189ff392b/code/animated_opt.gif -------------------------------------------------------------------------------- /code/asp.jl: -------------------------------------------------------------------------------- 1 | using FFTW # Perform fast Fourier transform 2 | 3 | """ 4 | ASP Use angular spectrum propagation (ASP) to propagate a scalar field 5 | from f0 = f(x=0,y) to f(x,y). 6 | 7 | === Input Arguments === 8 | f0 (numeric column vector or matrix): 9 | Initial field profile at x = 0 plane, as a column vector. 10 | If f0 has more than one column, each column is treated as a 11 | distinct initial field profile. size(f0,1) = ny. 12 | x (numeric scalar or row vector): 13 | Distance to propagate to. When multiple distances are of interest, 14 | they can be given as a row vector; the profiles f(x,y) at these 15 | distances will be returned as the different columns of f. When f0 16 | has more than one column, x must be a scalar (i.e., cannot 17 | propagate to multiple distances when multiple initial fields are 18 | given). 19 | kx_prop (N_prop-by-1 column vector): 20 | Longitudinal wave number kx. It can either (1) include all of the 21 | ny_tot wave numbers (both propagating and evanescent ones) in which 22 | case N_prop = length(kx_prop) must equal ny_tot and all of them 23 | will be propagated, or (2) include a subset of the wave numbers up 24 | to some cutoff in ky (e.g., the propagating ones) in which case 25 | N_prop = length(kx_prop) must be an odd number and only these 26 | components will be propagated. 27 | ny_tot (integer scalar; optional): 28 | Total number of grid points in y direction, including padded 29 | zeros. Must be no smaller than ny and N_prop. Defaults to ny. 30 | ny_pad_low (integer scalar; optional): 31 | Number of zeros to pad on the low side. Defaults to 32 | round((ny_tot-ny)/2). 33 | 34 | === Output Arguments === 35 | f (numeric column vector or matrix): 36 | Field profile(s) f(x,y). Different columns correspond to 37 | different initial profiles (if f0 has more than one column) or 38 | different distance x (if x is a row vector). 39 | """ 40 | 41 | function asp(f0::Union{Matrix{Int64}, Matrix{Float64}, Matrix{ComplexF64}, Vector{Int64}, Vector{Float64}, Vector{ComplexF64}}, x::Union{Int64, Float64, Array{Int64,2}, Array{Float64,2}}, kx_prop::Union{Vector{Int64}, Vector{Float64}, Vector{ComplexF64}}, ny_tot::Union{Int64, Nothing}, ny_pad_low::Union{Int64, Nothing}) 42 | 43 | ny = size(f0,1) 44 | 45 | N_prop = length(kx_prop) 46 | 47 | # no zero-padding unless ny_tot is given 48 | if isa(ny_tot, Nothing) 49 | ny_tot = ny 50 | elseif ny_tot < ny 51 | throw(ArgumentError("ny_tot, when given, must be no smaller than size(f0,1) = $(ny).")) 52 | elseif ny_tot < N_prop 53 | throw(ArgumentError("ny_tot, when given, must be no smaller than length(kx_prop) = $(N_prop).")) 54 | end 55 | 56 | # pad zeros symmetrically by default 57 | if isa(ny_pad_low, Nothing) 58 | ny_pad_low = Int(round((ny_tot-ny)/2)) 59 | elseif ny_pad_low + ny > ny_tot 60 | throw(ArgumentError("ny_pad_low + ny must be no greater than size(f0,1) = $(ny).")) 61 | end 62 | 63 | # Fourier transform f(x=0,y) to f(x=0,ky), as in Eq. (S43) of the APF paper. 64 | # To get a finer spacing in ky, zeros are padded below and above f0. 65 | # The most straightforward implementation is: 66 | # f0_fft = fft([zeros(ny_pad_low,size(f0,2)); f0; zeros(ny_tot-ny-ny_pad_low,size(f0,2))]); 67 | # But that comes with some redundant computations on the zeros. 68 | # The following is equivalent but slightly more efficient: 69 | f0_fft = exp.((-2im*pi*ny_pad_low/ny_tot).*collect(0:(ny_tot-1))).*fft([f0; zeros(ny_tot-ny,size(f0,2))],(1,)) 70 | 71 | # Remove the evanescent components of f(x=0,ky), propagate it to f(x,ky), 72 | # and ifft back to f(x,y), as in Eqs. (S41-S42) of the APF paper. 73 | # The most straightforward implementation is: 74 | # f_fft_prop = zeros(size(f0_fft)); 75 | # f_fft_prop(ind_prop,:) = exp(1i*kx_prop.*x).*f0_fft(ind_prop,:); 76 | # f = ifft(f_fft_prop); 77 | # But that comes with some redundant computations on the zeros. 78 | # The following is equivalent but slightly more efficient: 79 | if N_prop == ny_tot 80 | f = ifft(exp.(1im.*kx_prop.*x).*f0_fft, (1,)) 81 | else 82 | if mod(N_prop,2) != 1 83 | throw(ArgumentError("length(kx_prop) = $(N_prop) must be an odd number when it is not ny_tot.")) 84 | end 85 | a_max = Int(round((N_prop-1)/2)) 86 | ind_prop = vcat(collect(1:(a_max+1)), collect((ny_tot-a_max+1):ny_tot)) 87 | f_fft_prop = exp.(1im.*kx_prop.*x).*f0_fft[ind_prop,:] 88 | f = exp.((-2im*pi*a_max/ny_tot).*collect(0:(ny_tot-1))).*ifft([circshift(f_fft_prop, a_max); zeros(ny_tot-N_prop,size(f_fft_prop,2))], (1,)) 89 | end 90 | 91 | return f 92 | 93 | end 94 | 95 | # The following are asp functions to take different number of input arguments, but all of them will 96 | # call the asp main function. 97 | 98 | function asp(f0::Union{Matrix{Int64}, Matrix{Float64}, Matrix{ComplexF64}, Vector{Int64}, Vector{Float64}, Vector{ComplexF64}}, x::Union{Int64, Float64, Array{Int64,2}, Array{Float64,2}}, kx_prop::Union{Vector{Int64}, Vector{Float64}, Vector{ComplexF64}}) 99 | return asp(f0, x, kx_prop, nothing, nothing) 100 | end 101 | 102 | function asp(f0::Union{Matrix{Int64}, Matrix{Float64}, Matrix{ComplexF64}, Vector{Int64}, Vector{Float64}, Vector{ComplexF64}}, x::Union{Int64, Float64, Array{Int64,2}, Array{Float64,2}}, kx_prop::Union{Vector{Int64}, Vector{Float64}, Vector{ComplexF64}}, ny_tot::Union{Int64, Nothing}) 103 | return asp(f0, x, kx_prop, ny_tot, nothing) 104 | end 105 | -------------------------------------------------------------------------------- /code/inverse_design_WFOV_metalens.jl: -------------------------------------------------------------------------------- 1 | # In this file, we inverse design a metalens with wide field of view (FOV). 2 | # We change the permittivity of each design pixel inside the metalens, 3 | # maximizing the objective function (the product of transmission efficiency and Strehl ratio for each incident angle within the FOV of interest) [See Eq.(1) of the paper]. 4 | # The gradient of the objective function is computed using adjoint method [See Eq.(3) of the paper]. 5 | 6 | include("asp.jl") 7 | # The environmental variable for MUMPS3 (it should be the path to libraries of MUMPS) 8 | ENV["MUMPS_PREFIX"] = "the path to libraries of MUMPS" 9 | 10 | # Load the essential modules 11 | using MESTI # Used for full-wave simulations 12 | using Random # Used for the repeatability of random numbers 13 | using Interpolations # Perform interplations 14 | using FFTW # Perform fast Fourier transform 15 | using NLopt # Used to perform nonlinear optimization 16 | using Printf 17 | using LinearAlgebra 18 | using SparseArrays # Use sparse matrices to improve computational efficiency 19 | using MAT # Used to save data to *.mat file 20 | 21 | # Set the seed of random number generator to generate different initial guesses for reproducibility 22 | seed_rand = 01 23 | 24 | ################################# Define the simulation domain ##################################### 25 | 26 | n_air = 1.0 # Refractive index of air on the right 27 | n_sub = 1.0 # Refractive index of substrate on the left 28 | n_struct= 2.0 # Refractive index of silicon nitride 29 | wavelength = 1 # Vacuum wavelength 30 | dx = wavelength/40 # Discretization grid size [wavelength] 31 | FOV = 60 # FOV of the metalens in the air [deg] 32 | 33 | # Thickness of the metalens [wavelength] 34 | h = 5.0*wavelength 35 | # Output diameter of the metalens [wavelength] 36 | D_out = 50*wavelength 37 | # Input diameter of the metalens [wavelength] 38 | D_in = 25*wavelength 39 | # Numerical aperture NA 40 | NA = 0.9 41 | # Focal length [wavelength] 42 | focal_length = D_out/2/tan(asin(NA)) 43 | 44 | # Parameters for the input and output 45 | W_in = D_in # Input window width [wavelength] 46 | W_out = D_out + 2*wavelength # Width where we sample the transmitted field [wavelength] 47 | # (a larger W_out ensures all transmitted light is captured) 48 | 49 | # W_out > D_out, so we will pad extra pixels 50 | ny_R_extra_half = Int(round((W_out-D_out)/dx/2)) 51 | 52 | # Number of pixels in the y direction for the source (on the left) and the projection (on the right) 53 | ny = Int(ceil(D_out/dx)) 54 | ny_L = Int(ceil(D_in/dx)) 55 | ny_R = ny + 2*ny_R_extra_half 56 | # Number of pixels of the metasurface in the z direction 57 | nz = Int(ceil(h/dx)) 58 | 59 | nPML = 20 # Number of pixels of PMLs 60 | # PMLs on the z direction, add one pixel of free space for source and projection 61 | nz_extra_left = 1 + nPML 62 | nz_extra_right = nz_extra_left 63 | # PMLs on the y direction 64 | ny_extra_low = ny_R_extra_half + nPML 65 | ny_extra_high = ny_extra_low 66 | # Number of pixels for the whole system with PMLs 67 | ny_tot = ny + ny_extra_low + ny_extra_high 68 | nz_tot = nz + nz_extra_left + nz_extra_right 69 | 70 | nyz = ny*nz 71 | nyz_tot = ny_tot*nz_tot 72 | 73 | k0dx = 2*pi/wavelength*dx # Dimensionless frequency k0*dx 74 | epsilon_L = n_sub^2 # Relative permittivity on the left 75 | epsilon_R = n_air^2 # Relative permittivity on the right and on the top & bottom 76 | 77 | # Obtain properties of propagating channels on the two sides. 78 | BC = "periodic" # Periodic boundary condition means the propagating channels are plane waves 79 | use_continuous_dispersion = true # Use discrete dispersion relation for (kx,ky) 80 | channels_L = mesti_build_channels(ny, BC, k0dx, epsilon_L, nothing, use_continuous_dispersion) 81 | channels_R = mesti_build_channels(ny_R, BC, k0dx, epsilon_R, nothing, use_continuous_dispersion) 82 | 83 | # We use all propagating plane-wave channels on the right 84 | N_R = channels_R.N_prop # Number of channels on the right 85 | 86 | # For the incident plane waves, we only take channels within the desired FOV: 87 | # |ky| < n_air*k0*sin(FOV/2) 88 | kydx_bound = n_air*k0dx*sind((FOV+2)/2) # Use a slightly wider FOV to make sure that all incident angles of interest are included 89 | ind_kydx_FOV = findall(x-> (abs(x) < kydx_bound), channels_L.kydx_prop) 90 | kydx_FOV = channels_L.kydx_prop[ind_kydx_FOV] # kydx within the FOV 91 | kxdx_FOV = sqrt.((k0dx)^2 .- (kydx_FOV).^2) # kxdx within the FOV 92 | N_L = length(kydx_FOV) # Number of inputs 93 | 94 | B_basis = channels_L.f_x_m(channels_L.kydx_prop) # Build the propagating channels on the left of metalens 95 | C_R = conj(channels_R.f_x_m(channels_R.kydx_prop)) # Build the propagating channels on the right of metalens 96 | 97 | # Multiply the flux-normalized prefactor sqrt(nu) 98 | # Note that N_R includes all propagating channels on the right, but N_L 99 | # only includes propagating channels within the FOV on the left 100 | sqrt_nu_L_basis = reshape(channels_L.sqrt_nu_prop,1,:) # row vector 101 | sqrt_nu_R = channels_R.sqrt_nu_prop # column vector 102 | 103 | # We project each truncated input plane wave with |y|<=D_in/2 onto propagating channels and use their combinations as sources, such that the inputs satisfy the free-space wave equation. 104 | y_list = (collect(0.5:1:ny) .- ny/2).*dx 105 | ind_source_out = findall(x-> (abs(x) > D_in/2), y_list) # y coordinates outside of the input aperture 106 | B_trunc = B_basis[:,ind_kydx_FOV]*sqrt(ny/ny_L) 107 | B_trunc[ind_source_out,:] .= 0 # Block light outside of the input aperture 108 | 109 | B_L = zeros(ComplexF64,ny,N_L) 110 | for ii = 1:N_L 111 | for jj = 1:channels_L.N_prop 112 | Psi_in = sum(B_trunc[:,ii].*conj(B_basis[:,jj]),dims=1) 113 | B_L[:,ii] .= B_L[:,ii] .+ Psi_in*sqrt_nu_L_basis[jj]*exp((-1im*1/2)*channels_L.kzdx_prop[jj]).*B_basis[:,jj] 114 | end 115 | end 116 | 117 | # In mesti(), B_struct.pos = [m1, n1, h, w] specifies the position of a 118 | # block source, where (m1, n1) is the index of the smaller-(y,z) corner, 119 | # and (h, w) is the height and width of the block. Here, we put line 120 | # sources (w=1) on the left surface (n1=n_L) and the right surface 121 | # (n1=n_R) with height ny_L and ny_R centered around the metalens. 122 | n_L = nz_extra_left # z pixel immediately before the metalens 123 | n_R = n_L + nz + 1 # z pixel immediately after the metalens 124 | m1_L = ny_extra_low + 1 # first y pixel of the metalens 125 | m1_R = nPML + 1 # first y pixel of the output projection window 126 | 127 | ##################################### Angular Spectrum Propagation (ASP) ##################################### 128 | # System width used for ASP to remove periodic wrapping artifact. 129 | W_ASP_min = 2*D_out # Minimal ASP window [wavelength] 130 | 131 | # Since we want to extract the intensity at the exact focal spot position [See Eq.(1)] later, we keep a finer sampling here 132 | dy_ASP = 1*dx # ASP grid size [wavelength] 133 | 134 | # fft is more efficient when the length is a power of 2, so we make ny_ASP a power of 2. 135 | ny_ASP = nextpow(2,Int(round(W_ASP_min/dy_ASP))) # Number of pixels for ASP 136 | W_ASP = ny_ASP*dy_ASP # Actual ASP window [micron] 137 | 138 | # y index of the points we down-sample for ASP (if any). 139 | ind_ASP = Int.(collect(1:(dy_ASP/dx):ny_R)) 140 | 141 | # Make the sampling points symmetric around the middle. 142 | if ind_ASP[end] != ny_R 143 | # Make sure that ny_R - ind_ASP[end] is an even number. 144 | if mod(ny_R - ind_ASP[end], 2) != 0 145 | ind_ASP = ind_ASP[1:(end-1)] 146 | end 147 | ind_ASP = Int.(ind_ASP .+ (ny_R .- ind_ASP[end])./2) 148 | end 149 | 150 | ny_ASP_pad = ny_ASP - length(ind_ASP) # Total number of zeros to pad 151 | ny_ASP_pad_low = Int(round(ny_ASP_pad/2)) # Number of zeros to pad on the low side 152 | ny_ASP_pad_high = ny_ASP_pad - ny_ASP_pad_low # Number of zeros to pad on the high side 153 | 154 | # y position of the ASP points, including the padded zeros [micron] 155 | y_ASP = (collect(0.5:ny_ASP) .- 0.5*(ny_ASP + ny_ASP_pad_low - ny_ASP_pad_high))*dy_ASP 156 | 157 | ny_ASP_half = Int(round(ny_ASP/2)) # recall that ny_ASP is an even number 158 | 159 | # List of (kx,ky) in ASP 160 | # Note that after fft, ky_ASP will be (2*pi/W_ASP)*(0:(ny_ASP-1)), where the 161 | # latter half needs to be unwrapped to negative ky. We can either use fftshift 162 | # and iffshift while centering ky_ASP, or just unwrap ky_ASP itself; we do the 163 | # latter here. 164 | ky_ASP = (2*pi/W_ASP).*[collect(0:(ny_ASP_half-1)); collect(-ny_ASP_half:-1)] 165 | kx_ASP = sqrt.(Complex.((n_air*2*pi/wavelength)^2 .- ky_ASP.^2)) 166 | 167 | # We only use the propagating components in ASP 168 | kx_ASP_prop = kx_ASP[findall(x-> (abs(x) < (n_air*2*pi/wavelength)), ky_ASP)] # must be a column vector per asp() syntax 169 | 170 | # List of incident angles in air [degree] 171 | # n_air*sin(theta_in) = n_sub*sin(theta_sub), 172 | # where theta_sub = atan(channels_L.kydx/channels_L.kxdx) 173 | theta_in_list = asind.(sin.(atan.(kydx_FOV./kxdx_FOV)).*n_sub./n_air) 174 | 175 | # Later we will use ifft to reconstruct field profile immediately after the 176 | # metalens from the transmission matrix, i.e. Eq (S7) of the APF paper, 177 | # and this is the prefactor we need. 178 | prefactor_ifft = sqrt(ny_R).*exp.((-2im*pi/ny_R*(N_R-1)/2).*collect(0:(ny_R-1))) 179 | 180 | # Focal spot position for each incident angle 181 | focal_spot_list = focal_length.*tand.(theta_in_list) 182 | interp_linear = linear_interpolation(y_ASP,collect(1:ny_ASP)) 183 | ind_plot = Int.(round.(interp_linear(focal_spot_list))) 184 | # Extract field information at the desired focal spot position for each incident angle from ASP results 185 | y_plot = y_ASP[ind_plot] 186 | 187 | ################################ Ideal focusing under different incident angles ##################################### 188 | # Focal field & focal intensity of an ideal metalens free of aberrations 189 | focal_intensity_ideal = zeros(N_L,1) 190 | focal_field_ideal = zeros(ny_ASP,N_L) 191 | for ii = 1:N_L 192 | ideal_phase = -(2*pi/wavelength).*sqrt.(focal_length^2 .+ (y_list.-focal_spot_list[ii]).^2) 193 | # Ideal field on the exit surface of the metalens [See Eq.(S5) of the Supplementary document] 194 | # Outside the metalens, the Ez_ideal is zero. 195 | Ez_ideal = [zeros(ny_R_extra_half,1); exp.(1im*ideal_phase)./(focal_length^2 .+(y_list.-focal_spot_list[ii]).^2).^(1/4); zeros(ny_R_extra_half,1)] 196 | 197 | # Project Ez_ideal onto the propagating channels on the right 198 | temp = circshift(exp.(-1im*2*pi/ny_R.*collect(0:ny_R-1)).*fft(Ez_ideal), Int(floor(channels_R.N_prop/2)))./sqrt(ny_R) 199 | t_ideal = (channels_R.sqrt_nu_prop).*temp[1:channels_R.N_prop] 200 | 201 | # Use ASP to propagate Ez_ideal to the focal plane 202 | Ez0 = circshift(prefactor_ifft.*ifft([(1 ./sqrt_nu_R).*t_ideal; zeros(ny_R-N_R,1)],(1,)), -1) 203 | Ez0_ASP = Ez0[ind_ASP,:] 204 | Ez_focal_ideal = asp(Ez0_ASP, focal_length, kx_ASP_prop, ny_ASP, ny_ASP_pad_low) 205 | 206 | # Ideal focal field (set angle-dependent prefactor A in Eq.(S5) of the Supplementary document to ensure unitary transmission) 207 | focal_field_ideal[:,ii] = abs.(Ez_focal_ideal).^2/sum(abs.(t_ideal).^2) 208 | # Ideal focal intensity (See the denominator of Eq.(1)) 209 | focal_intensity_ideal[ii] = maximum(abs.(Ez_focal_ideal))^2/sum(abs.(t_ideal).^2) 210 | end 211 | 212 | ######################### Integrate ASP into projection matrix C (See Supplementary Sec.1) ########################### 213 | pad_matrix_1 = [Matrix{Int64}(I, N_R, N_R); zeros(ny_R-N_R, N_R)] 214 | num_ind_ASP = length(ind_ASP) 215 | ind_ASP_matrix = zeros(num_ind_ASP, ny_R) 216 | for ii = 1:num_ind_ASP 217 | ind_ASP_matrix[ii,ind_ASP[ii]] = 1 218 | end 219 | 220 | pad_matrix_2 = [zeros(ny_ASP_pad_high,num_ind_ASP); Matrix{Int64}(I, num_ind_ASP, num_ind_ASP); zeros(ny_ASP_pad_low,num_ind_ASP)] 221 | 222 | ind_ASP_prop = findall(x-> (real(x)>0), kx_ASP) 223 | num_ASP_prop = length(ind_ASP_prop) 224 | ind_ASP_prop_matrix = zeros(num_ASP_prop, ny_ASP) 225 | for ii = 1:num_ASP_prop 226 | ind_ASP_prop_matrix[ii,ind_ASP_prop[ii]] = 1 227 | end 228 | 229 | kx_prop_matrix = exp.(1im*focal_length.*kx_ASP_prop) 230 | pad_matrix_3 = zeros(ny_ASP, num_ASP_prop) 231 | pad_matrix_3[ind_ASP_prop,:] = Matrix{Int64}(I, num_ASP_prop, num_ASP_prop) 232 | 233 | n_ind_prop = length(ind_plot) 234 | ind_plot_matrix = zeros(n_ind_prop,ny_ASP) 235 | for ii = 1:n_ind_prop 236 | ind_plot_matrix[ii,ind_plot[ii]] = 1 237 | end 238 | 239 | C_R_new = ind_plot_matrix*ifft(pad_matrix_3*(kx_prop_matrix.*ind_ASP_prop_matrix*fft(pad_matrix_2*ind_ASP_matrix*circshift(prefactor_ifft.*ifft(pad_matrix_1*transpose(C_R),(1,)), -1),(1,))),(1,)) 240 | 241 | # Compute inv(A)*[B, C^T] for both the forward simulation and gradient information (See Eq.(3) and Supplementary Sec.3 of the paper) 242 | # 2-element structure array B_struct 243 | B_struct = Source_struct() 244 | B_struct.pos = [[m1_L, n_L, ny, 1], [m1_R, n_R, ny_R, 1]] 245 | B_struct.data = [B_L, permutedims(C_R_new, (2,1))] 246 | 247 | # Multiply matrix C to inv(A)*B, and get the focal field of an actual design at the desired focus positions 248 | # First, we build matrix C 249 | m2 = m1_R + ny_R - 1 # last index in y 250 | n2 = n_R # last index in z 251 | # Stack reshaped B_R with zeros to build matrix C. 252 | nyz_before = (n_R-1)*ny_tot + m1_R - 1 253 | nyz_after = ny_tot*nz_tot - ((n2-1)*ny_tot+m2) 254 | C_matrix = hcat(spzeros(N_L, nyz_before), C_R_new, spzeros(N_L, nyz_after)) 255 | 256 | ############################### Initialize the simulation ############################################ 257 | # Update the permittivity profile with a macropixel size of 4*dx to reduce the dimension of the design space and keep the minimal feature size large for the ease of fabrication 258 | # Build a metalens with inequal input and output aperture sizes 259 | # Keep track of the indices of the updated pixels 260 | ny_opt = Int(ny/4) 261 | ny_L_opt = Int(ny_L/4) 262 | nz_opt = Int(nz/4) 263 | increment_per_layer = (ny_opt - ny_L_opt)/2/(nz_opt-1) 264 | num_struct = 0 265 | for ii = 1:nz_opt 266 | global num_struct += Int(round(ny_L_opt/2 + (ii-1)*increment_per_layer)) 267 | end 268 | y_list_opt = (collect(0.5:1:ny_opt) .- ny_opt/2).*dx*4 # coordinates in the y direction 269 | ind_struct_left = vec(zeros(num_struct,1)) 270 | num_struct_init = 0 271 | for ii = 1:nz_opt 272 | ind_struct = findall(x-> (-x<=(D_in/2+increment_per_layer*(ii-1)*dx*4)), y_list_opt[1:Int(ny_opt/2)]) 273 | ind_struct_left[num_struct_init.+(1:length(ind_struct))] = Base._sub2ind((ny_opt,nz_opt), ind_struct, ii*vec(Int.(ones(length(ind_struct),1)))) 274 | global num_struct_init += length(ind_struct) 275 | end 276 | ind_struct_left = Int.(ind_struct_left) 277 | 278 | ind_struct_right = vec(zeros(num_struct,1)) 279 | num_struct_init = 0 280 | for ii = 1:nz_opt 281 | ind_struct = findall(x-> (-x<=(D_in/2+increment_per_layer*(ii-1)*dx*4)), y_list_opt[Int(ny_opt/2):-1:1]) 282 | ind_struct_right[num_struct_init.+(1:length(ind_struct))] = Base._sub2ind((ny_opt,nz_opt), (Int(ny_opt/2)).+reverse(ind_struct,dims=1), ii*vec(Int.(ones(length(ind_struct),1)))) 283 | global num_struct_init += length(ind_struct) 284 | end 285 | ind_struct_right = Int.(ind_struct_right) 286 | 287 | ind_epsilon_left = zeros(4^2*num_struct,1) 288 | ind_epsilon_right = zeros(4^2*num_struct,1) 289 | for ii = 1:num_struct 290 | epsilon_ind_opt = zeros(ny_opt,nz_opt) 291 | epsilon_ind_opt[ind_struct_left[ii]] = 1 292 | epsilon_ind = zeros(ny_tot,nz_tot) 293 | epsilon_ind[ny_extra_low.+(1:ny),nz_extra_left.+(1:nz)] = kron(epsilon_ind_opt,ones(4,4)) 294 | ind_epsilon_left[(ii-1)*16 .+ (1:4^2)] = findall(x-> (x > 0), epsilon_ind[:]) 295 | temp = getindex.(vec(reverse(reshape(findall(x-> (x > 0), [zeros(Int(ny_tot/2),nz_tot);reverse(epsilon_ind[1:Int(ny_tot/2),:],dims=1)]),(4,4)),dims=1)),[1 2]) 296 | ind_epsilon_right[(ii-1)*16 .+ (1:4^2)] = Base._sub2ind((ny_tot,nz_tot),temp[:,1],temp[:,2]) 297 | end 298 | ind_epsilon_left = vec(Int.(ind_epsilon_left)) 299 | ind_epsilon_right = vec(Int.(ind_epsilon_right)) 300 | 301 | # Generate random initial guesses 302 | epsilon = rand(MersenneTwister(seed_rand),Float64,(num_struct,1)) 303 | 304 | # Construct the permittivity profile over all simulation domain (including 305 | # metalens, PML, and extra padded pixels) 306 | epsilon_real_opt = ones(ny_opt,nz_opt) 307 | epsilon_real_opt[ind_struct_left] = epsilon*n_struct^2 .+ (1 .- epsilon)*n_air^2 308 | epsilon_real = ones(ny_tot,nz_tot) 309 | epsilon_real[ny_extra_low.+(1:ny),nz_extra_left.+(1:nz)] = kron(epsilon_real_opt,ones(4,4)) # Use a finer discretization of dx compared to the minimal feature size 4*dx for accurate simulations 310 | 311 | # The metalens is mirror symmetric with respect to y=0 312 | syst = Syst() 313 | syst.epsilon_xx = [epsilon_real[1:Int(ny_tot/2),:]; reverse(epsilon_real[1:Int(ny_tot/2),:],dims=1)] 314 | syst.length_unit = "µm" 315 | syst.wavelength = wavelength 316 | syst.dx = dx 317 | syst.PML = [PML(nPML)] # Number of PML pixels (on all four sides) 318 | 319 | # Use MUMPS with single-precision arithmetic to improve the computational efficiency 320 | opts = Opts() 321 | opts.method = "FS" 322 | opts.solver = "MUMPS" 323 | opts.use_single_precision_MUMPS = true 324 | 325 | # Use MESTI to run simulations of the metalens, and propagate to the desired focal plane via ASP 326 | B_struct_i = Source_struct() 327 | C_struct_i = Source_struct() 328 | B_struct_i.pos = [[m1_L, n_L, ny, 1]] 329 | B_struct_i.data = [B_L] 330 | C_struct_i.pos = [[m1_R, n_R, ny_R, 1]] 331 | C_struct_i.data = [permutedims(C_R_new, (2,1))] 332 | (T_init, info) = mesti(syst, [B_struct_i], [C_struct_i], opts) 333 | T_init = T_init*(-2im) 334 | 335 | # Build the initial guess of the optimizations (dummy variable g and permittivity) [see Eqs.(1-2) of the paper] 336 | # The first element is the dummy variable g <= I_a 337 | x_init = [minimum(abs.(diag(T_init)).^2 ./focal_intensity_ideal); vec(epsilon)] 338 | 339 | # Factors related to the binarization (See Supplementary Sec.5) 340 | norm_norm = norm(ones(num_struct,1) .- 0.5) 341 | max_eval = 400 342 | 343 | ######################### Define the objective function and inequality constraints ##################### 344 | # Define the objective function with a binarization regularizer (See Eq.(2) and Supplementary Sec.5) and its gradient 345 | function obj_func(x::Vector{Float64}, grad::Vector{Float64}, norm_norm, max_eval) 346 | # Gradient of the objective function + regularizer 347 | if length(grad) > 0 348 | grad[:] = vec(zeros(length(x),1)) 349 | grad[1] = 1 350 | if opt.numevals < max_eval 351 | grad[2:end] = 2*(opt.numevals/max_eval)^2/norm_norm^2*(x[2:end] .- 0.5) 352 | else 353 | grad[2:end] = 2/norm_norm^2*(x[2:end] .- 0.5) 354 | end 355 | end 356 | # Objective function + regularizer 357 | if opt.numevals < max_eval 358 | @printf("two-material factor = %7.6f\n", norm(x[2:end] .- 0.5)/norm_norm) 359 | return x[1] + 1*(opt.numevals/max_eval)^2*(norm(x[2:end] .- 0.5)/norm_norm)^2 # x[1] is a dummy variable determined by the nonlinear inequality 360 | else 361 | @printf("two-material factor = %7.6f\n", norm(x[2:end] .- 0.5)/norm_norm) 362 | return x[1] + 1*(norm(x[2:end] .- 0.5)/norm_norm)^2 # x[1] is a dummy variable determined by the nonlinear inequality 363 | end 364 | end 365 | 366 | # Define inequality constraints and its gradient (See Eqs.(2-3) and Supplementary Sec.3) 367 | function constraint_func(result::Vector{Float64}, x::Vector{Float64}, grad::Matrix{Float64}, syst::Syst, B::Vector{Source_struct}, C_matrix::Union{SparseMatrixCSC{Int64,Int64},SparseMatrixCSC{Float64, Int64},SparseMatrixCSC{ComplexF64,Int64}}, opts::Opts, n_struct::Float64, n_air::Float64, k0dx::Union{Float64,ComplexF64}, ny::Int64, nz::Int64, ny_extra_low::Int64, nz_extra_left::Int64, ny_tot::Int64, nz_tot::Int64, N_L::Int64, sqrt_nu_R::Vector{Float64}, focal_intensity_ideal::Matrix{Float64},ind_struct_left::Vector{Int64},ind_struct_right::Vector{Int64},ind_epsilon_left::Vector{Int64},ind_epsilon_right::Vector{Int64}) 368 | # Gradient of the nonlinear constraints (Eq.(3) and Supplementary Sec.3 of the paper) 369 | if length(grad) > 0 370 | # Build the permittivity profile of the metalens 371 | epsilon_real_opt = ones(ny_opt,nz_opt) 372 | epsilon_real_opt[ind_struct_left] = x[2:end]*n_struct^2 .+ (1 .- x[2:end])*n_air^2 373 | epsilon_real = ones(ny_tot,nz_tot) 374 | epsilon_real[ny_extra_low.+(1:ny),nz_extra_left.+(1:nz)] = kron(epsilon_real_opt,ones(4,4)) 375 | syst.epsilon_xx = [epsilon_real[1:Int(ny_tot/2),:]; reverse(epsilon_real[1:Int(ny_tot/2),:],dims=1)] 376 | 377 | num_ind_left = length(ind_struct_left) 378 | 379 | opts.verbal = false # Surpress verbal outputs 380 | # Compute inv(A)*[B,C^T] 381 | (field, info) = mesti(syst, B, opts) 382 | 383 | nyz_tot = ny_tot*nz_tot 384 | field = reshape(field, nyz_tot, N_L*2) 385 | 386 | AB = field[:, 1:N_L]*(-2im) # Extract inv(A)*B 387 | AC = field[:, N_L+1:end] # Extract inv(A)*C^T 388 | field = nothing # clear field 389 | 390 | # Get T = C*inv(A)*B, get the focal field of an actual design at the desired focus positions 391 | T_ang = C_matrix*AB 392 | 393 | # Get the gradient with respect to all optimization variables and incident angles 394 | AB_design_1 = reshape(AB[ind_epsilon_left,:],4^2,num_ind_left,N_L) 395 | AC_design_1 = reshape(AC[ind_epsilon_left,:].*(diag(T_ang)./focal_intensity_ideal)',4^2,num_ind_left,N_L) 396 | AB_design_2 = reshape(AB[ind_epsilon_right,:],4^2,num_ind_left,N_L) 397 | AC_design_2 = reshape(AC[ind_epsilon_right,:].*(diag(T_ang)./focal_intensity_ideal)',4^2,num_ind_left,N_L) 398 | 399 | grad .= 1 .*ones(length(ind_struct_left)+1,N_L) 400 | grad[2:end,:] = -2*k0dx^2*(n_struct^2-n_air^2)*dropdims(real(sum(AC_design_1.*AB_design_1,dims=1)).+real(sum(AC_design_2.*AB_design_2,dims=1));dims=1) 401 | end 402 | # Nonlinear constraints (Eq.(2) of the paper) 403 | result[:] = vec(-abs.(diag(T_ang)).^2 ./focal_intensity_ideal .+ x[1]) 404 | @printf("FoM = %7.6f, t = %7.6f at Iteration %d\n", minimum(abs.(diag(T_ang)).^2 ./focal_intensity_ideal), x[1], opt.numevals) 405 | 406 | end 407 | 408 | ###################### Perform the optimization using NLopt ##################################### 409 | opt = Opt(:LD_MMA, num_struct+1) # Specify the optimization algorithm: MMA 410 | opt.lower_bounds = [0; vec(zeros(num_struct,1))] # Lower bounds of optimization variables 411 | opt.upper_bounds = [1; vec(ones(num_struct,1))] # Upper bounds of optimization variables 412 | opt.xtol_abs = [1e-7; 1e-20*vec(ones(num_struct,1))] # Absolute tolerance of optimization variables 413 | opt.maxtime = 1*16*60*60 # Maximal optimization time 414 | 415 | # Build nonlinear constraints 416 | opt.max_objective = (x,g) -> obj_func(x,g,norm_norm,max_eval) 417 | inequality_constraint!(opt, (r,x,g) -> constraint_func(r,x,g,syst,[B_struct],C_matrix,opts,n_struct,n_air,k0dx,ny,nz,ny_extra_low,nz_extra_left,ny_tot,nz_tot,N_L,sqrt_nu_R,focal_intensity_ideal,ind_struct_left,ind_struct_right,ind_epsilon_left,ind_epsilon_right), 1e-2*vec(ones(N_L,1))) 418 | 419 | # Run optimizations using Nlopt 420 | (maxf,maxx,ret) = optimize!(opt, x_init) 421 | numevals = opt.numevals # the number of function evaluations 422 | println("got $maxf after $numevals iterations (returned $ret)") 423 | 424 | 425 | ###################################### Obtain the optimized results ##################################### 426 | # Get I_a = SR_a*T_a for each incident angle [Eq.(1) of the paper] 427 | epsilon_real_opt = ones(ny_opt,nz_opt) 428 | epsilon_real_opt[ind_struct_left] = maxx[2:end]*n_struct^2 .+ (1 .- maxx[2:end])*n_air^2 429 | epsilon_real = ones(ny_tot,nz_tot) 430 | epsilon_real[ny_extra_low.+(1:ny),nz_extra_left.+(1:nz)] = kron(epsilon_real_opt,ones(4,4)) 431 | syst.epsilon_xx = [epsilon_real[1:Int(ny_tot/2),:]; reverse(epsilon_real[1:Int(ny_tot/2),:],dims=1)] 432 | C_struct_i.data = [permutedims(C_R_new, (2,1))] 433 | (T_fin, info) = mesti(syst, [B_struct_i], [C_struct_i], opts) 434 | T_fin = T_fin*(-2im) 435 | FoM_opt = abs.(diag(T_fin)).^2 ./focal_intensity_ideal 436 | 437 | # Compute the transmission efficiency T_a for each incident angle [Supplementary Eq.(44) of the APF paper] 438 | C_struct_i = Source_struct() 439 | C_struct_i.pos = [[m1_R, n_R, ny_R, 1]] 440 | C_struct_i.data = [C_R] 441 | (T_eff, info) = mesti(syst, [B_struct_i], [C_struct_i], opts) 442 | T_eff = sqrt_nu_R.*T_eff*(-2im) 443 | T_actual = sum(abs.(T_eff).^2, dims=1) 444 | 445 | # Get the Strehl ratio for each incident angle 446 | SR = FoM_opt./vec(T_actual) 447 | 448 | # Save optimized data 449 | matwrite("optimized_data.mat", Dict( 450 | "FoM_opt" => FoM_opt, 451 | "T_actual" => T_actual, 452 | "SR" => SR, 453 | "theta_in_list" => theta_in_list 454 | ); compress = true) 455 | --------------------------------------------------------------------------------