diff --git a/metaprop_fibermode_doublet.py b/metaprop_fibermode_doublet.py index ff9b1b1..b7db74a 100644 --- a/metaprop_fibermode_doublet.py +++ b/metaprop_fibermode_doublet.py @@ -26,19 +26,23 @@ if not os.path.exists("results"): ############################## #physics and simulation domain wavelength = 1.55e-6 -n0 = 1.0 +n0 = 1.45 n1 = 1.0 -n2 = 1.0 +n2 = 1.45 -k0 = 2 * np.pi / wavelength -k1 = k0 -k2 = k0 +lda0 = wavelength / n0 +lda1 = wavelength / n1 +lda2 = wavelength / n2 -opt_max_eval = 250 +k0 = 2 * np.pi / lda0 +k1 = 2 * np.pi / lda1 +k2 = 2 * np.pi / lda2 + +opt_max_eval = 150 unit_cell_pitch = 700e-9 #equivalent to spatial sampling -Nx = 2048 #pixels per dimension +Nx = 1024 #pixels per dimension Ny = Nx S = Nx * Ny @@ -53,10 +57,9 @@ rho = np.sqrt(X**2 + Y**2) sampling_period = xs[1] - xs[0] -d0 = 300e-6 #propagation distance: source - MS1 -d1 = 500e-6 #propagation distance: MS1 - MS2 -d2 = 400e-6 #propagation distance: MS2 - target -d = [d1, d2] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target +d0 = 403e-6 #propagation distance: source - MS1 +d1 = 158e-6 #propagation distance: MS1 - MS2 +d2 = d0 #propagation distance: MS2 - target phase_min = 0 phase_max = 2 * np.pi @@ -88,13 +91,13 @@ KX, KY = np.meshgrid(kappas, kappas) K_parallel = np.sqrt(KX**2 + KY**2) nu_parallel = K_parallel / (2 * np.pi) -phase_factor_0 = k0 * d0 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +phase_factor_0 = k0 * d0 * np.sqrt(1 - (lda0 * nu_parallel) ** 2 + 0*1j) P_0 = np.exp(1j * phase_factor_0) P_0_nat = np.fft.ifftshift(P_0) -phase_factor_1 = k0 * d1 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +phase_factor_1 = k1 * d1 * np.sqrt(1 - (lda1 * nu_parallel) ** 2 + 0*1j) P_1 = np.exp(1j * phase_factor_1) P_1_nat = np.fft.ifftshift(P_1) -phase_factor_2 = k0 * d2 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +phase_factor_2 = k2 * d2 * np.sqrt(1 - (lda2 * nu_parallel) ** 2 + 0*1j) P_2 = np.exp(1j * phase_factor_2) P_2_nat = np.fft.ifftshift(P_2) @@ -106,7 +109,6 @@ P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj() ############################## #Plot zoom parameters [um] -#Plot zoom parameters [um] zoom_x = 150 zoom_y = 150 zoom_output_fiber_radius = 63 @@ -146,7 +148,7 @@ def get_fiber_mode_pattern(mode_list:list)->list: for enum_index, n in enumerate(mode_list): #try to load mode index n try: - pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy" + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" pattern = np.load(pattern_file) output_patterns.append(pattern) print("Loaded fiber patterns for mode index: ", n) @@ -209,7 +211,7 @@ def get_fiber_mode_pattern(mode_list:list)->list: else:#outside core E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi) #save the computed pattern to a file for future use - pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy" + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" np.save(pattern_file, E_field) output_patterns.append(E_field) return output_patterns @@ -366,6 +368,8 @@ make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field #initialize input field (propagated source field) input_field = propagate_source(source_field) input_field_2d = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#save the input field to numpy array +np.save("results/input_field.npy", input_field_2d) #input field integrated intensity input_field_intensity = np.sum(np.abs(input_field)**2) print("Input field integrated intensity: %.4e" % input_field_intensity) @@ -376,7 +380,7 @@ make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_p ########################### #initialize target field -target_mode_indices = [1] +target_mode_indices = [2] fiber_mode_pattern = get_fiber_mode_pattern(target_mode_indices) target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2)) target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1) @@ -416,7 +420,7 @@ def forward_propagate() -> None: print("Starting propagating...") start_time = time.time() - fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)) + fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))) fft_operator() #print("BEFORE") fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S @@ -434,14 +438,14 @@ def forward_propagate() -> None: #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) ifft_operator() - intermediate_fields[0] = ifft_operator.output_array.copy() + intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy()) print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2)) - fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:].reshape((Nx, Ny)))) + fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny)))) fft_operator() ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product ifft_operator() - intermediate_fields[1] = ifft_operator.output_array.copy() + intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy()) end_time = time.time() #make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) @@ -462,19 +466,19 @@ def adjoint_propagate(): Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny)) #compute the adjoint source - fft_input_array[:,:] = np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d + fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d) fft_operator() ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat ifft_operator() - fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger + fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger) - grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * fft_input_array).flatten() + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten() fft_operator() ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise ifft_operator() - grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten() + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten() end_time = time.time() print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) @@ -494,6 +498,7 @@ def _compute_cost(): return C_t opt_history = [] +iter_num = [0] def cost_fun(x, grad): ''' Cost function to be minimized. Args: @@ -509,15 +514,16 @@ def cost_fun(x, grad): C = _compute_cost() opt_history.append(C) - - plt.figure() - plt.plot(opt_history) - plt.xlabel('Iteration') - plt.ylabel('Cost Function Value') - plt.title('Optimization History') - plt.tight_layout() - plt.savefig("results/modeconv1_optimization_history.pdf") - plt.close() + iter_num[0] += 1 + if iter_num[0] % 10 == 0: + plt.figure() + plt.plot(opt_history) + plt.xlabel('Iteration') + plt.ylabel('Cost Function Value') + plt.title('Optimization History') + plt.tight_layout() + plt.savefig("results/optimization_history.pdf") + plt.close() if grad.size > 0: grad[:] = adjoint_propagate() * 2 * np.pi @@ -539,10 +545,11 @@ if __name__ == "__main__": if start: w_norm[:] = solver.optimize(w_norm) print("Optimization completed.") - + print("LAST OPTIMUM VALUE: ", solver.last_optimum_value()) #verify the results phase_mask = phase_given_w(w_norm) forward_propagate() + field_input_ms2 = intermediate_fields[0] output_field = intermediate_fields[-1] input_power = np.sum(np.abs(input_field)**2) @@ -552,9 +559,10 @@ if __name__ == "__main__": print("Norm. output power: %.4e" % output_power) #save the output field for later use - save_output_field = False + save_output_field = True if save_output_field: - np.savetxt("results/optimized_output_field.txt", output_field.reshape((Nx, Ny))) + np.save("results/intermediate_field_0.npy", intermediate_fields[0].reshape((Nx, Ny))) + np.save("results/optimized_output_field.npy", output_field.reshape((Nx, Ny))) # Plot results in individual PDF figures # Reshape fields to 2D for visualization @@ -580,7 +588,7 @@ if __name__ == "__main__": #save the phase masks for later use savePhase_masks = True if savePhase_masks: - np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1) - np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2) + np.save("results/optimized_phase_mask_1.npy", phase_mask_1) + np.save("results/optimized_phase_mask_2.npy", phase_mask_2) print("All plots saved to results/ folder.") \ No newline at end of file diff --git a/metaprop_fibermode_doublet_distGrad.py b/metaprop_fibermode_doublet_distGrad.py new file mode 100644 index 0000000..c23302b --- /dev/null +++ b/metaprop_fibermode_doublet_distGrad.py @@ -0,0 +1,692 @@ +import time +import multiprocessing + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +import os + +if not os.path.exists("results"): + os.makedirs("results") + +############################## +#physics and simulation domain +wavelength = 1.55e-6 +n0 = 1.0 +n1 = 1.0 +n2 = 1.0 + +lda0 = wavelength / n0 +lda1 = wavelength / n1 +lda2 = wavelength / n2 + +k0 = 2 * np.pi / lda0 +k1 = 2 * np.pi / lda1 +k2 = 2 * np.pi / lda2 + +opt_max_eval = 250 + +unit_cell_pitch = 700e-9 #equivalent to spatial sampling + +Nx = 2048 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d0_init = 300e-6 #propagation distance: source - MS1 +d1_init = 500e-6 #propagation distance: MS1 - MS2 +d2_init = 400e-6 #propagation distance: MS2 - target +distance_num = 3 +d = np.array([d0_init, d1_init, d2_init]) + +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#Source +beam_waist = 5.2e-6 +#output fibre +output_fiber_core_diameter = 19e-6 +############################## + +############################## +#plan pyfftw objects for forward and inverse FFTs +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = [] +P_0 = [] +P_0_nat = [] +phase_factor_1 = [] +P_1 = [] +P_1_nat = [] +phase_factor_2 = [] +P_2 = [] +P_2_nat = [] +P_1_dagger = [] +P_2_dagger = [] +P_1_dagger_nat = [] +P_2_dagger_nat = [] + +############################## +def compute_phase_factors(): + phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_0[:] = np.exp(1j * phase_factor_0) + P_0_nat[:] = np.fft.ifftshift(P_0) + phase_factor_1[:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_1[:] = np.exp(1j * phase_factor_1) + P_1_nat[:] = np.fft.ifftshift(P_1) + phase_factor_2[:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_2[:] = np.exp(1j * phase_factor_2) + P_2_nat[:] = np.fft.ifftshift(P_2) + + P_1_dagger[:] = P_1.T.conj() + P_2_dagger[:] = P_2.T.conj() + P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj() + P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj() + + +############################## +#Plot zoom parameters [um] +#Plot zoom parameters [um] +zoom_x = 150 +zoom_y = 150 +zoom_output_fiber_radius = 63 +full_view_x = size_x * 1e6 / 2 +full_view_y = size_y * 1e6 / 2 +############################## + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(mode_list:list)->list: + '''Returns the fiber mode pattern for the selected n index. + Args: + mode_list: the list of the selected fiber mode indices + Returns: + The fiber mode patterns corresponding to the selected indices returned + as a list of 2D arrays with the same order as the input mode_list + ''' + #check if the modes in mode_list have already been compute by checking + #the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files. + fiber_modes_input_folder = "fiber_modes" + output_patterns = [] + try: + for enum_index, n in enumerate(mode_list): + #try to load mode index n + try: + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy" + pattern = np.load(pattern_file) + output_patterns.append(pattern) + print("Loaded fiber patterns for mode index: ", n) + #if mode index n was not found, compute it + except FileNotFoundError: + if not os.path.exists(fiber_modes_input_folder): + os.makedirs(fiber_modes_input_folder) + + print("Computing fiber patterns for mode index: ", n) + lda = 1.55e-6 + + n_co = 1.4630 #core refractive index + n_cl = 1.4585 #cladding refractive index + + fiber_core_diameter = output_fiber_core_diameter # [m] + a = fiber_core_diameter / 2 #fiber core radius + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + + b_solutions_at_V_obs = [] + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + b_mode_sel = b_solutions_at_V_obs[0] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi) + else:#outside core + E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi) + #save the computed pattern to a file for future use + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy" + np.save(pattern_file, E_field) + output_patterns.append(E_field) + return output_patterns + except: + raise(Exception("Error in computing fiber patterns")) + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the simulation spatial grid by a given physical (true space) amount. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X + shift_amount_x + Yp = Y + shift_amount_y + return Xp, Yp + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates. + Normalization consists in scaling the field such that its integrated intensity is 1. + + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The 2D normalized Gaussian source field. + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + + return gaussian_field_normalized + +def propagate_source(source_to_propagate): + '''Returns the propagated source field (flattened) just before metasurface 1 (MS1). + This involves propagating the source for a distance d0: from its plane of definition up to MS1. + Args: + source_to_propagate: the source field to be propagated (flattened) + Returns: + The propagated source field (flattened) just before MS1. + ''' + + fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten() + + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + plt.plot(x_axis[i], y_axis[i]) + plt.xlabel("x [um]") + plt.tight_layout() + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None: + '''Makes a polar plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_r: zoom level for the radial axis (in micrometers) + Returns: + None + ''' + fig, ax = plt.subplots(figsize=(6, 6)) + if choose_quantity == "amplitude": + given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting + im = ax.imshow(given_field_2d,origin='lower') + elif choose_quantity == "phase": + given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting + im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + center_x, center_y = Nx//2, Ny//2 + radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels + circle = patches.Circle((center_x, center_y), radius, transform=ax.transData) + im.set_clip_path(circle) + ax.set_xlim(center_x - radius, center_x + radius) + ax.set_ylim(center_y - radius, center_y + radius) + ax.axis('off') + cbar = plt.colorbar(im, ax=ax) + plt.savefig("results/" + save_name + ".pdf") + +########################### +#initialize source field +source_field = get_source_field(beam_waist) +source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#source field integrated intensity +source_power = np.sum(np.abs(source_field)**2) +print("Source field integrated intensity: %.4e" % source_power) +#make a plot and save the source field +make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +########################### + +########################### +#initialize input field (propagated source field) +input_field = [] +input_field_2d = [] + +#make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +#make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +def compute_input_field(): + input_field[:] = propagate_source(source_field) + input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting + #input field integrated intensity + input_field_intensity = np.sum(np.abs(input_field)**2) + print("Input field integrated intensity: %.4e" % input_field_intensity) +########################### + +########################### +#initialize target field +target_mode_indices = [1] +fiber_mode_pattern = get_fiber_mode_pattern(target_mode_indices) +target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2)) +target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1) + +# Plot: Target field +make_polarPlot_of(target_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius) +########################### + +########################### +#initialize the normalized phase masks and global phase mask +w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks +X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny)) +rho = np.sqrt(X**2 + Y**2) +circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0) +#w_norm[:S] += circular_mask_1 +target_pattern_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi +w_norm[S:] += target_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 +phase_mask = phase_given_w(w_norm) + +#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. +#the bounding box is the outer shell of a square with thickness 100 um +norm_phase_min = 0 +norm_phase_max = 1 +mask_buffer = 100e-6 +mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) +mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) +mask_lower_bounds = np.zeros_like(mask_upper_bounds) +########################### + +intermediate_fields = [None, None] +def forward_propagate() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields[0] = ifft_operator.output_array.copy() + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2)) + + fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:].reshape((Nx, Ny)))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields[1] = ifft_operator.output_array.copy() + + end_time = time.time() + #make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + +def adjoint_propagate(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * fft_input_array).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + +def dist_grad(): + P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2) + P_0_triangle = P_0 * P_0_tilde + P_0_triangle_nat = np.fft.ifftshift(P_0_triangle) + + P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2) + P_1_triangle = P_1 * P_1_tilde + P_1_triangle_nat = np.fft.ifftshift(P_1_triangle) + + P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2) + P_2_triangle = P_2 * P_2_tilde + P_2_triangle_nat = np.fft.ifftshift(P_2_triangle) + + Phi_1 = np.exp(1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2 = np.exp(1j * phase_mask[S:]).reshape((Nx, Ny)) + S = np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) + S_dagger = np.conj(S) + E_t_dagger = np.conj(target_Efield_2d) + + ################################ + #dC_d0 + fft_input_array[:,:] = input_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d1 + fft_input_array[:,:] = input_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d2 + fft_input_array[:,:] = input_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat + ifft_operator() + + dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + + return np.concatenate((dC_d0, dC_d1, dC_d2)) + +def _compute_cost(): + + forward_propagate() + + output_field = intermediate_fields[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field * np.conj(target_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +opt_history = [] +def cost_fun(x, grad): + ''' Cost function to be minimized. + Args: + x: input normalized parameter field (flattened) + grad: gradient of the cost function with respect to x, modified in place (flattened) + Returns: + The cost function value for the input parameter field x. + ''' + d[:] = x[:distance_num] #update distance variables + + w_norm[:] = x[distance_num:] #update normalized phase mask parameters + phase_mask[:] = phase_given_w(w_norm) + + compute_phase_factors() + compute_input_field() + + C = _compute_cost() + + opt_history.append(C) + + plt.figure() + plt.plot(opt_history) + plt.xlabel('Iteration') + plt.ylabel('Cost Function Value') + plt.title('Optimization History') + plt.tight_layout() + plt.savefig("results/modeconv1_optimization_history.pdf") + plt.close() + + if grad.size > 0: + grad[:] = adjoint_propagate() * 2 * np.pi + return C + +if __name__ == "__main__": + #initialize nlopt solver + solver = nlopt.opt(nlopt.LD_CCSAQ, distance_num + 2*S) + + solver.set_lower_bounds(mask_lower_bounds.flatten()) + solver.set_upper_bounds(mask_upper_bounds.flatten()) + solver.set_max_objective(cost_fun) + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-7) + solver.set_param("verbosity",1) + + print("Starting optimization...") + start = True + dist_and_w_norm = np.concatenate((d, w_norm), axis=0) #initialize optimization variables + + if start: + dist_and_w_norm[:] = solver.optimize(dist_and_w_norm) + + print("Optimization completed.") + d[:] = dist_and_w_norm[:distance_num] #update distance variables + w_norm[:] = dist_and_w_norm[distance_num:] #update normalized phase mask parameters + #verify the results + compute_phase_factors() + compute_input_field() + phase_mask = phase_given_w(w_norm) + forward_propagate() + output_field = intermediate_fields[-1] + + input_power = np.sum(np.abs(input_field)**2) + output_power = np.sum(np.abs(output_field)**2) + + print("Norm. input power: %.4e" % input_power) + print("Norm. output power: %.4e" % output_power) + + #save the output field for later use + save_output_field = False + if save_output_field: + np.savetxt("results/optimized_output_field.txt", output_field.reshape((Nx, Ny))) + # Plot results in individual PDF figures + + # Reshape fields to 2D for visualization + output_field_2d = output_field.reshape((Nx, Ny)) + phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(w_norm[S:]).reshape((Nx, Ny)) + + phase_mask_zoom = 150 + make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + + # Plot: Output field amplitude and phase + make_polarPlot_of(output_field_2d, choose_quantity="amplitude", save_name="optimized_output_field_amplitude", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field_2d, choose_quantity="phase", save_name="optimized_output_field_phase", plot_zoom_r=zoom_output_fiber_radius) + + slice_y_index = Ny//2 + slice_x = xs * 1e6 + slice_target = target_Efield.reshape((Nx, Ny))[slice_y_index,:] + slice_output = output_field.reshape((Nx, Ny))[slice_y_index,:] + make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude") + make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase") + + #save the phase masks for later use + savePhase_masks = True + if savePhase_masks: + np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1) + np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2) + + print("All plots saved to results/ folder.") \ No newline at end of file diff --git a/metaprop_fibermode_doublet_distGrad_xy.py b/metaprop_fibermode_doublet_distGrad_xy.py new file mode 100644 index 0000000..f7fc09f --- /dev/null +++ b/metaprop_fibermode_doublet_distGrad_xy.py @@ -0,0 +1,900 @@ +import gc +import time +import multiprocessing + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +import os + +if not os.path.exists("results"): + os.makedirs("results") + +############################## +#physics and simulation domain +wavelength = 1.55e-6 +n0 = 1.0 +n1 = 1.0 +n2 = 1.0 + +lda0 = wavelength / n0 +lda1 = wavelength / n1 +lda2 = wavelength / n2 + +k0 = 2 * np.pi / lda0 +k1 = 2 * np.pi / lda1 +k2 = 2 * np.pi / lda2 + +opt_max_eval = 150 + +unit_cell_pitch = 700e-9 #equivalent to spatial sampling + +Nx = 2048 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d0_init = 300e-6 #propagation distance: source - MS1 +d1_init = 500e-6 #propagation distance: MS1 - MS2 +d2_init = 400e-6 #propagation distance: MS2 - target +distance_lower_bound = 100e-6 +distance_upper_bound = 1000e-6 +distance_num = 3 +d = np.array([d0_init, d1_init, d2_init]) + +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#Source +beam_waist = 5.2e-6 +#output fibre +output_fiber_core_diameter = 19e-6 +############################## + +############################## +#plan pyfftw objects for forward and inverse FFTs +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = np.zeros((Nx, Ny), dtype=complex) +P_0 = np.zeros((Nx, Ny), dtype=complex) +P_0_nat = np.zeros((Nx, Ny), dtype=complex) +phase_factor_1 = np.zeros((Nx, Ny), dtype=complex) +P_1 = np.zeros((Nx, Ny), dtype=complex) +P_1_nat = np.zeros((Nx, Ny), dtype=complex) +phase_factor_2 = np.zeros((Nx, Ny), dtype=complex) +P_2 = np.zeros((Nx, Ny), dtype=complex) +P_2_nat = np.zeros((Nx, Ny), dtype=complex) +P_1_dagger = np.zeros((Nx, Ny), dtype=complex) +P_2_dagger = np.zeros((Nx, Ny), dtype=complex) +P_1_dagger_nat = np.zeros((Nx, Ny), dtype=complex) +P_2_dagger_nat = np.zeros((Nx, Ny), dtype=complex) + +############################## +def compute_phase_factors(): + phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_0[:] = np.exp(1j * phase_factor_0) + P_0_nat[:] = np.fft.ifftshift(P_0) + phase_factor_1[:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_1[:] = np.exp(1j * phase_factor_1) + P_1_nat[:] = np.fft.ifftshift(P_1) + phase_factor_2[:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_2[:] = np.exp(1j * phase_factor_2) + P_2_nat[:] = np.fft.ifftshift(P_2) + + P_1_dagger[:] = P_1.T.conj() + P_2_dagger[:] = P_2.T.conj() + P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj() + P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj() + + +############################## +#Plot zoom parameters [um] +#Plot zoom parameters [um] +zoom_x = 150 +zoom_y = 150 +zoom_output_fiber_radius = 63 +full_view_x = size_x * 1e6 / 2 +full_view_y = size_y * 1e6 / 2 +############################## + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(mode_list:list)->list: + '''Returns the fiber mode pattern for the selected n index. + Args: + mode_list: the list of the selected fiber mode indices + Returns: + The fiber mode patterns corresponding to the selected indices returned + as a list of 2D arrays with the same order as the input mode_list + ''' + #check if the modes in mode_list have already been compute by checking + #the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files. + fiber_modes_input_folder = "fiber_modes" + output_patterns = [] + try: + for enum_index, n in enumerate(mode_list): + #try to load mode index n + try: + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy" + pattern = np.load(pattern_file) + output_patterns.append(pattern) + print("Loaded fiber patterns for mode index: ", n) + #if mode index n was not found, compute it + except FileNotFoundError: + if not os.path.exists(fiber_modes_input_folder): + os.makedirs(fiber_modes_input_folder) + + print("Computing fiber patterns for mode index: ", n) + lda = 1.55e-6 + + n_co = 1.4630 #core refractive index + n_cl = 1.4585 #cladding refractive index + + fiber_core_diameter = output_fiber_core_diameter # [m] + a = fiber_core_diameter / 2 #fiber core radius + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + + b_solutions_at_V_obs = [] + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + b_mode_sel = b_solutions_at_V_obs[0] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi) + else:#outside core + E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi) + #save the computed pattern to a file for future use + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy" + np.save(pattern_file, E_field) + output_patterns.append(E_field) + return output_patterns + except: + raise(Exception("Error in computing fiber patterns")) + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the simulation spatial grid by a given physical (true space) amount. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X + shift_amount_x + Yp = Y + shift_amount_y + return Xp, Yp + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates. + Normalization consists in scaling the field such that its integrated intensity is 1. + + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The 2D normalized Gaussian source field. + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + + return gaussian_field_normalized + +def propagate_source(source_to_propagate): + '''Returns the propagated source field (flattened) just before metasurface 1 (MS1). + This involves propagating the source for a distance d0: from its plane of definition up to MS1. + Args: + source_to_propagate: the source field to be propagated (flattened) + Returns: + The propagated source field (flattened) just before MS1. + ''' + + fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten() + + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + plt.plot(x_axis[i], y_axis[i]) + plt.xlabel("x [um]") + plt.tight_layout() + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None: + '''Makes a polar plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_r: zoom level for the radial axis (in micrometers) + Returns: + None + ''' + fig, ax = plt.subplots(figsize=(6, 6)) + if choose_quantity == "amplitude": + given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting + im = ax.imshow(given_field_2d,origin='lower') + elif choose_quantity == "phase": + given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting + im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + center_x, center_y = Nx//2, Ny//2 + radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels + circle = patches.Circle((center_x, center_y), radius, transform=ax.transData) + im.set_clip_path(circle) + ax.set_xlim(center_x - radius, center_x + radius) + ax.set_ylim(center_y - radius, center_y + radius) + ax.axis('off') + cbar = plt.colorbar(im, ax=ax) + plt.savefig("results/" + save_name + ".pdf") + +########################### +#initialize source field +source_field = get_source_field(beam_waist) +source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#source field integrated intensity +source_power = np.sum(np.abs(source_field)**2) +print("Source field integrated intensity: %.4e" % source_power) +#make a plot and save the source field +make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +########################### + +########################### +#initialize input field (propagated source field) +input_field = np.zeros((S,), dtype=complex) +input_field_2d = np.zeros((Nx, Ny), dtype=complex) + +#make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +#make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +def compute_input_field(): + input_field[:] = propagate_source(source_field) + input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting + #input field integrated intensity + input_field_intensity = np.sum(np.abs(input_field)**2) + print("Input field integrated intensity: %.4e" % input_field_intensity) +########################### + +########################### +#initialize target field +target_mode_indices = [1,2] +fiber_mode_pattern1, fiber_mode_pattern2 = get_fiber_mode_pattern(target_mode_indices) +target1_Efield_2d = fiber_mode_pattern1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern1)**2)) +target2_Efield_2d = fiber_mode_pattern2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern2)**2)) +target1_Efield = target1_Efield_2d.flatten() #normalized target field (intensity = 1) +target2_Efield = target2_Efield_2d.flatten() #normalized target field (intensity = 1) + +# Plot: Target field +make_polarPlot_of(target1_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target1_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target2_Efield_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target2_Efield_2d, choose_quantity="phase", save_name="target_field_2_phase_polar", plot_zoom_r=zoom_output_fiber_radius) +########################### + +########################### +#initialize the normalized phase masks and global phase mask +w_norm = np.zeros((4*S)) #concatenated normalized parameters for both masks +X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny)) +rho = np.sqrt(X**2 + Y**2) +circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0) +#w_norm[:S] += circular_mask_1 +target1_pattern_phase = (np.angle(target1_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi +w_norm[S:2*S] += target1_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 + +target2_pattern_phase = (np.angle(target2_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi +w_norm[3*S:4*S] += target2_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 + +phase_mask = phase_given_w(w_norm) + +#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. +#the bounding box is the outer shell of a square with thickness 100 um +norm_phase_min = 0 +norm_phase_max = 1 +mask_buffer = 100e-6 +mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) +mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) +mask_lower_bounds = np.zeros_like(mask_upper_bounds) +distance_upper_bounds = np.array([1, 1, 1]) +distance_lower_bounds = np.array([0, 0, 0]) + +distance_and_mask_upper_bounds = np.concatenate((distance_upper_bounds, mask_upper_bounds, mask_upper_bounds),axis=0) +distance_and_mask_lower_bounds = np.concatenate((distance_lower_bounds, mask_lower_bounds, mask_lower_bounds),axis=0) +########################### + +intermediate_fields_1 = [None, None] +intermediate_fields_2 = [None, None] +def forward_propagate_1() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields_1[0] = ifft_operator.output_array.copy() + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_1[0])**2)) + + fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:2*S].reshape((Nx, Ny)))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields_1[1] = ifft_operator.output_array.copy() + + end_time = time.time() + make_2Dplot_of(intermediate_fields_1[0], choose_quantity="amplitude", save_name="intermediate_field_1_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(intermediate_fields_1[1], choose_quantity="amplitude", save_name="intermediate_field_1_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + +def forward_propagate_2() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[2*S:3*S])).reshape((Nx, Ny)) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields_2[0] = ifft_operator.output_array.copy() + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_2[0])**2)) + + fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[3*S:4*S].reshape((Nx, Ny)))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields_2[1] = ifft_operator.output_array.copy() + + end_time = time.time() + #make_2Dplot_of(intermediate_fields_2[0], choose_quantity="amplitude", save_name="intermediate_field_2_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(intermediate_fields_2[1], choose_quantity="amplitude", save_name="intermediate_field_2_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + +def adjoint_propagate_1(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:2*S]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d)) * target1_Efield_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_1[0].conj() * fft_input_array).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + +def adjoint_propagate_2(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[2*S:3*S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[3*S:4*S]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d)) * target2_Efield_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_2[0].conj() * fft_input_array).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + +def dist_grad_1(): + P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j) + print("shape of P_0_tilde: ", P_0_tilde.shape) + P_0_triangle = P_0 * P_0_tilde + P_0_triangle_nat = np.fft.ifftshift(P_0_triangle) + + P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j) + P_1_triangle = P_1 * P_1_tilde + P_1_triangle_nat = np.fft.ifftshift(P_1_triangle) + + P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j) + P_2_triangle = P_2 * P_2_tilde + P_2_triangle_nat = np.fft.ifftshift(P_2_triangle) + + Phi_1 = np.exp(1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2 = np.exp(1j * phase_mask[S:2*S]).reshape((Nx, Ny)) + Sf = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d)) + S_dagger = np.conj(Sf) + E_t_dagger = np.conj(target1_Efield_2d) + + ################################ + #dC_d0 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d1 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d2 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat + ifft_operator() + + dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + + return np.array([dC_d0, dC_d1, dC_d2]) + +def dist_grad_2(): + P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j) + print("shape of P_0_tilde: ", P_0_tilde.shape) + P_0_triangle = P_0 * P_0_tilde + P_0_triangle_nat = np.fft.ifftshift(P_0_triangle) + + P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j) + P_1_triangle = P_1 * P_1_tilde + P_1_triangle_nat = np.fft.ifftshift(P_1_triangle) + + P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j) + P_2_triangle = P_2 * P_2_tilde + P_2_triangle_nat = np.fft.ifftshift(P_2_triangle) + + Phi_1 = np.exp(1j * phase_mask[2*S:3*S]).reshape((Nx, Ny)) + Phi_2 = np.exp(1j * phase_mask[3*S:4*S]).reshape((Nx, Ny)) + Sf = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d)) + S_dagger = np.conj(Sf) + E_t_dagger = np.conj(target2_Efield_2d) + + ################################ + #dC_d0 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d1 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d2 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat + ifft_operator() + + dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + + return np.array([dC_d0, dC_d1, dC_d2]) + +def dist_norm_to_real(norm_dist): + '''Maps normalized distance parameters to real distance values. + Args: + norm_dist: normalized distance parameters (flattened) + Returns: + Real distance values corresponding to the normalized parameters (flattened)''' + return norm_dist * (distance_upper_bound - distance_lower_bound) + distance_lower_bound + +def _compute_cost_1(): + + forward_propagate_1() + + output_field_1 = intermediate_fields_1[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field_1 * np.conj(target1_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +def _compute_cost_2(): + + forward_propagate_2() + + output_field_2 = intermediate_fields_2[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field_2 * np.conj(target2_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +opt_history_1 = [] +opt_history_2 = [] +opt_cycle = [0] +def cost_fun(x, grad): + ''' Cost function to be minimized. + Args: + x: input normalized parameter field (flattened) + grad: gradient of the cost function with respect to x, modified in place (flattened) + Returns: + The cost function value for the input parameter field x. + ''' + d[:] = dist_norm_to_real(x[:distance_num]) #update distance variables + print("Current distances: ", d) + w_norm[:] = x[distance_num:] #update normalized phase mask parameters + phase_mask[:] = phase_given_w(w_norm) + + compute_phase_factors() #update phase factor matrices + compute_input_field() #update input field based on the new distances + + C1 = _compute_cost_1() + C2 = _compute_cost_2() + C = np.mean([C1, C2]) #average cost function for both modes + opt_history_1.append(C1) + opt_history_2.append(C2) + + if opt_cycle[0] % 10 == 0: + plt.figure() + plt.plot(opt_history_1, 'r', label="Mode #1", markersize=2) + plt.plot(opt_history_2, 'b', label="Mode #2", markersize=2) + plt.xlabel('Iteration') + plt.ylabel('Cost Function Value') + plt.title('Optimization History') + plt.tight_layout() + plt.legend() + plt.savefig("results/modeconv1_optimization_history.pdf") + plt.close() + + opt_cycle[0] = opt_cycle[0] + 1 + if grad.size > 0: + if opt_cycle[0] < 1000: + grad[:distance_num] = np.zeros(distance_num) #set distance gradient to zero for the first 100 iterations + else: + grad[:distance_num] = dist_grad_1() * (distance_upper_bound - distance_lower_bound) + dist_grad_2() * (distance_upper_bound - distance_lower_bound) + #grad[:distance_num] = np.array([0,0,0]) #set distance gradient to zero for now + grad[distance_num:2*S+distance_num] = adjoint_propagate_1() * 2 * np.pi * 0.5 + grad[2*S+distance_num:4*S+distance_num] = adjoint_propagate_2() * 2 * np.pi * 0.5 + gc.collect() #force garbage collection to free memory + return C + +if __name__ == "__main__": + #initialize nlopt solver + solver = nlopt.opt(nlopt.LD_CCSAQ, distance_num + 4*S) + + solver.set_lower_bounds(distance_and_mask_lower_bounds.flatten()) + solver.set_upper_bounds(distance_and_mask_upper_bounds.flatten()) + solver.set_max_objective(cost_fun) + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-7) + solver.set_param("verbosity",1) + + print("Starting optimization...") + start = True + initial_d = np.array([0.4, 0.5, 0.3]) + dist_and_w_norm = np.concatenate((initial_d, w_norm), axis=0) #initialize optimization variables + + if start: + dist_and_w_norm[:] = solver.optimize(dist_and_w_norm) + + print("Optimization completed.") + d[:] = dist_norm_to_real(dist_and_w_norm[:distance_num]) #update distance variables + w_norm[:] = dist_and_w_norm[distance_num:] #update normalized phase mask parameters + print("Optimized distances: ", d) + #verify the results + compute_phase_factors() + compute_input_field() + phase_mask[:] = phase_given_w(w_norm) + forward_propagate_1() + forward_propagate_2() + output_field1 = intermediate_fields_1[-1] + output_field2 = intermediate_fields_2[-1] + + input_power = np.sum(np.abs(input_field)**2) + output_power = np.sum(np.abs(output_field1)**2) + + print("Norm. input power: %.4e" % input_power) + print("Norm. output power: %.4e" % output_power) + + #save the output field for later use + save_output_field = False + if save_output_field: + np.savetxt("results/optimized_output_field.txt", output_field1.reshape((Nx, Ny))) + # Plot results in individual PDF figures + + # Reshape fields to 2D for visualization + output_field1_2d = output_field1.reshape((Nx, Ny)) + output_field2_2d = output_field2.reshape((Nx, Ny)) + phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(w_norm[S:2*S]).reshape((Nx, Ny)) + phase_mask_3 = phase_given_w(w_norm[2*S:3*S]).reshape((Nx, Ny)) + phase_mask_4 = phase_given_w(w_norm[3*S:4*S]).reshape((Nx, Ny)) + + phase_mask_zoom = 150 + make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_3, choose_quantity="phase_mask", save_name="phase_mask_3", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_4, choose_quantity="phase_mask", save_name="phase_mask_4", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + + # Plot: Output field amplitude and phase + make_polarPlot_of(output_field1_2d, choose_quantity="amplitude", save_name="optimized_output_field_amplitude", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field1_2d, choose_quantity="phase", save_name="optimized_output_field_phase", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field2_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field2_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius) + + slice_y_index = Ny//2 + slice_x = xs * 1e6 + slice_target = target1_Efield.reshape((Nx, Ny))[slice_y_index,:] + slice_output = output_field1.reshape((Nx, Ny))[slice_y_index,:] + #make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude") + #make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase") + + #save the phase masks for later use + savePhase_masks = False + if savePhase_masks: + np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1) + np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2) + + print("All plots saved to results/ folder.") \ No newline at end of file diff --git a/metaprop_fibermode_doublet_distGrad_xy_epigraph.py b/metaprop_fibermode_doublet_distGrad_xy_epigraph.py new file mode 100644 index 0000000..1fba6c7 --- /dev/null +++ b/metaprop_fibermode_doublet_distGrad_xy_epigraph.py @@ -0,0 +1,954 @@ +''' +This file implements a metasurface doublet for mode conversion. The optimization loop implements two beam paths that are +optimized independetly. This is useful for example when considering distinct polarizations (x and y). The optimization +is based on an epigraph formulation. +''' + +import gc +import time +import multiprocessing + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +import os + +if not os.path.exists("results"): + os.makedirs("results") + +############################## +#physics and simulation domain +wavelength = 1.55e-6 +n0 = 1.0 +n1 = 1.0 +n2 = 1.0 + +lda0 = wavelength / n0 +lda1 = wavelength / n1 +lda2 = wavelength / n2 + +k0 = 2 * np.pi / lda0 +k1 = 2 * np.pi / lda1 +k2 = 2 * np.pi / lda2 + +opt_max_eval = 270 + +unit_cell_pitch = 700e-9 #equivalent to spatial sampling + +Nx = 1024 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d0_init = 300e-6 #propagation distance: source - MS1 +d1_init = 500e-6 #propagation distance: MS1 - MS2 +d2_init = 300e-6 #propagation distance: MS2 - target +distance_lower_bound = 100e-6 +distance_upper_bound = 1000e-6 +distance_num = 3 +d = np.array([d0_init, d1_init, d2_init]) + +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#Source +beam_waist = 5.2e-6 +#output fibre +output_fiber_core_diameter = 19e-6 +############################## + +############################## +#plan pyfftw objects for forward and inverse FFTs +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = np.zeros((Nx, Ny), dtype=complex) +P_0 = np.zeros((Nx, Ny), dtype=complex) +P_0_nat = np.zeros((Nx, Ny), dtype=complex) +phase_factor_1 = np.zeros((Nx, Ny), dtype=complex) +P_1 = np.zeros((Nx, Ny), dtype=complex) +P_1_nat = np.zeros((Nx, Ny), dtype=complex) +phase_factor_2 = np.zeros((Nx, Ny), dtype=complex) +P_2 = np.zeros((Nx, Ny), dtype=complex) +P_2_nat = np.zeros((Nx, Ny), dtype=complex) +P_1_dagger = np.zeros((Nx, Ny), dtype=complex) +P_2_dagger = np.zeros((Nx, Ny), dtype=complex) +P_1_dagger_nat = np.zeros((Nx, Ny), dtype=complex) +P_2_dagger_nat = np.zeros((Nx, Ny), dtype=complex) + +############################## +def compute_phase_factors(): + phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_0[:] = np.exp(1j * phase_factor_0) + P_0_nat[:] = np.fft.ifftshift(P_0) + phase_factor_1[:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_1[:] = np.exp(1j * phase_factor_1) + P_1_nat[:] = np.fft.ifftshift(P_1) + phase_factor_2[:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_2[:] = np.exp(1j * phase_factor_2) + P_2_nat[:] = np.fft.ifftshift(P_2) + + P_1_dagger[:] = P_1.T.conj() + P_2_dagger[:] = P_2.T.conj() + P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj() + P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj() + + +############################## +#Plot zoom parameters [um] +#Plot zoom parameters [um] +zoom_x = 150 +zoom_y = 150 +zoom_output_fiber_radius = 25 +full_view_x = size_x * 1e6 / 2 +full_view_y = size_y * 1e6 / 2 +############################## + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(mode_list:list)->list: + '''Returns the fiber mode pattern for the selected n index. + Args: + mode_list: the list of the selected fiber mode indices + Returns: + The fiber mode patterns corresponding to the selected indices returned + as a list of 2D arrays with the same order as the input mode_list + ''' + #check if the modes in mode_list have already been compute by checking + #the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files. + fiber_modes_input_folder = "fiber_modes" + output_patterns = [] + try: + for enum_index, n in enumerate(mode_list): + #try to load mode index n + try: + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" + pattern = np.load(pattern_file) + output_patterns.append(pattern) + print("Loaded fiber patterns for mode index: ", n) + #if mode index n was not found, compute it + except FileNotFoundError: + if not os.path.exists(fiber_modes_input_folder): + os.makedirs(fiber_modes_input_folder) + + print("Computing fiber patterns for mode index: ", n) + lda = 1.55e-6 + + n_co = 1.4630 #core refractive index + n_cl = 1.4585 #cladding refractive index + + fiber_core_diameter = output_fiber_core_diameter # [m] + a = fiber_core_diameter / 2 #fiber core radius + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + + b_solutions_at_V_obs = [] + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + b_mode_sel = b_solutions_at_V_obs[0] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi) + else:#outside core + E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi) + #save the computed pattern to a file for future use + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" + np.save(pattern_file, E_field) + output_patterns.append(E_field) + return output_patterns + except: + raise(Exception("Error in computing fiber patterns")) + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the simulation spatial grid by a given physical (true space) amount. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X + shift_amount_x + Yp = Y + shift_amount_y + return Xp, Yp + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates. + Normalization consists in scaling the field such that its integrated intensity is 1. + + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The 2D normalized Gaussian source field. + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + + return gaussian_field_normalized + +def propagate_source(source_to_propagate): + '''Returns the propagated source field (flattened) just before metasurface 1 (MS1). + This involves propagating the source for a distance d0: from its plane of definition up to MS1. + Args: + source_to_propagate: the source field to be propagated (flattened) + Returns: + The propagated source field (flattened) just before MS1. + ''' + + fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten() + + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + plt.plot(x_axis[i], y_axis[i]) + plt.xlabel("x [um]") + plt.tight_layout() + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None: + '''Makes a polar plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_r: zoom level for the radial axis (in micrometers) + Returns: + None + ''' + fig, ax = plt.subplots(figsize=(6, 6)) + if choose_quantity == "amplitude": + given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting + im = ax.imshow(given_field_2d,origin='lower') + elif choose_quantity == "phase": + given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting + im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("argument choose_quantity must be either 'amplitude' or 'phase'") + + center_x, center_y = Nx//2, Ny//2 + radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels + circle = patches.Circle((center_x, center_y), radius, transform=ax.transData) + im.set_clip_path(circle) + ax.set_xlim(center_x - radius, center_x + radius) + ax.set_ylim(center_y - radius, center_y + radius) + ax.axis('off') + cbar = plt.colorbar(im, ax=ax) + plt.savefig("results/" + save_name + ".pdf") + +########################### +#initialize source field +source_field = get_source_field(beam_waist) +source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#source field integrated intensity +source_power = np.sum(np.abs(source_field)**2) +print("Source field integrated intensity: %.4e" % source_power) +#make a plot and save the source field +make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +########################### + +########################### +#initialize input field (propagated source field) +input_field = np.zeros((S,), dtype=complex) +input_field_2d = np.zeros((Nx, Ny), dtype=complex) + +#make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +#make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +def compute_input_field(): + input_field[:] = propagate_source(source_field) + input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting + #input field integrated intensity + input_field_intensity = np.sum(np.abs(input_field)**2) + print("Input field integrated intensity: %.4e" % input_field_intensity) +########################### + +########################### +#initialize target field +target_mode_indices = [1,2] +fiber_mode_pattern1, fiber_mode_pattern2 = get_fiber_mode_pattern(target_mode_indices) +target1_Efield_2d = fiber_mode_pattern1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern1)**2)) +target2_Efield_2d = fiber_mode_pattern2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern2)**2)) +target1_Efield = target1_Efield_2d.flatten() #normalized target field (intensity = 1) +target2_Efield = target2_Efield_2d.flatten() #normalized target field (intensity = 1) + +# Plot: Target field +make_polarPlot_of(target1_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target1_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target2_Efield_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target2_Efield_2d, choose_quantity="phase", save_name="target_field_2_phase_polar", plot_zoom_r=zoom_output_fiber_radius) +########################### + +########################### +#initialize the normalized phase masks and global phase mask +w_norm = np.zeros((4*S)) #concatenated normalized parameters for both masks +X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny)) +rho = np.sqrt(X**2 + Y**2) +circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0) +#w_norm[:S] += circular_mask_1 +target1_pattern_phase = (np.angle(target1_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi +w_norm[S:2*S] += target1_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 + +target2_pattern_phase = (np.angle(target2_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi +w_norm[3*S:4*S] += target2_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 + +phase_mask = phase_given_w(w_norm) + +#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. +#the bounding box is the outer shell of a square with thickness 100 um +norm_phase_min = 0 +norm_phase_max = 1 +mask_buffer = 100e-6 +mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) +mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) +mask_lower_bounds = np.zeros_like(mask_upper_bounds) +distance_upper_bounds = np.array([1, 1, 1]) +distance_lower_bounds = np.array([0, 0, 0]) +epigraph_upper_bounds = np.array([+np.inf]) +epigraph_lower_bounds = np.array([-np.inf]) +epigraph_distance_and_mask_upper_bounds = np.concatenate((epigraph_upper_bounds, distance_upper_bounds, mask_upper_bounds, mask_upper_bounds),axis=0) +epigraph_distance_and_mask_lower_bounds = np.concatenate((epigraph_lower_bounds, distance_lower_bounds, mask_lower_bounds, mask_lower_bounds),axis=0) +########################### + +intermediate_fields_1 = [None, None] +intermediate_fields_2 = [None, None] +def forward_propagate_1() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields_1[0] = ifft_operator.output_array.copy() + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_1[0])**2)) + + fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:2*S].reshape((Nx, Ny)))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields_1[1] = ifft_operator.output_array.copy() + + end_time = time.time() + make_2Dplot_of(intermediate_fields_1[0], choose_quantity="amplitude", save_name="intermediate_field_1_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(intermediate_fields_1[1], choose_quantity="amplitude", save_name="intermediate_field_1_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + +def forward_propagate_2() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[2*S:3*S])).reshape((Nx, Ny)) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields_2[0] = ifft_operator.output_array.copy() + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_2[0])**2)) + + fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[3*S:4*S].reshape((Nx, Ny)))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields_2[1] = ifft_operator.output_array.copy() + + end_time = time.time() + #make_2Dplot_of(intermediate_fields_2[0], choose_quantity="amplitude", save_name="intermediate_field_2_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(intermediate_fields_2[1], choose_quantity="amplitude", save_name="intermediate_field_2_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + +def adjoint_propagate_1(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:2*S]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d)) * target1_Efield_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_1[0].conj() * fft_input_array).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + +def adjoint_propagate_2(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[2*S:3*S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[3*S:4*S]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d)) * target2_Efield_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_2[0].conj() * fft_input_array).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + +def dist_grad_1(): + P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j) + #print("shape of P_0_tilde: ", P_0_tilde.shape) + P_0_triangle = P_0 * P_0_tilde + P_0_triangle_nat = np.fft.ifftshift(P_0_triangle) + + P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j) + P_1_triangle = P_1 * P_1_tilde + P_1_triangle_nat = np.fft.ifftshift(P_1_triangle) + + P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j) + P_2_triangle = P_2 * P_2_tilde + P_2_triangle_nat = np.fft.ifftshift(P_2_triangle) + + Phi_1 = np.exp(1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2 = np.exp(1j * phase_mask[S:2*S]).reshape((Nx, Ny)) + Sf = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d)) + S_dagger = np.conj(Sf) + E_t_dagger = np.conj(target1_Efield_2d) + + ################################ + #dC_d0 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d1 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d2 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat + ifft_operator() + + dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + + return np.array([dC_d0, dC_d1, dC_d2]) + +def dist_grad_2(): + P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j) + print("shape of P_0_tilde: ", P_0_tilde.shape) + P_0_triangle = P_0 * P_0_tilde + P_0_triangle_nat = np.fft.ifftshift(P_0_triangle) + + P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j) + P_1_triangle = P_1 * P_1_tilde + P_1_triangle_nat = np.fft.ifftshift(P_1_triangle) + + P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j) + P_2_triangle = P_2 * P_2_tilde + P_2_triangle_nat = np.fft.ifftshift(P_2_triangle) + + Phi_1 = np.exp(1j * phase_mask[2*S:3*S]).reshape((Nx, Ny)) + Phi_2 = np.exp(1j * phase_mask[3*S:4*S]).reshape((Nx, Ny)) + Sf = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d)) + S_dagger = np.conj(Sf) + E_t_dagger = np.conj(target2_Efield_2d) + + ################################ + #dC_d0 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d1 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d2 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat + ifft_operator() + + dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + + return np.array([dC_d0, dC_d1, dC_d2]) + +def dist_norm_to_real(norm_dist): + '''Maps normalized distance parameters to real distance values. + Args: + norm_dist: normalized distance parameters (flattened) + Returns: + Real distance values corresponding to the normalized parameters (flattened)''' + return norm_dist * (distance_upper_bound - distance_lower_bound) + distance_lower_bound + +def _compute_cost_1(): + + forward_propagate_1() + + output_field_1 = intermediate_fields_1[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field_1 * np.conj(target1_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +def _compute_cost_2(): + + forward_propagate_2() + + output_field_2 = intermediate_fields_2[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field_2 * np.conj(target2_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +opt_history_1 = [] +opt_history_2 = [] +opt_cycle = [0] +def cost_fun(x): + ''' Cost function to be minimized. + Args: + x: input normalized parameter field (flattened) + grad: gradient of the cost function with respect to x, modified in place (flattened) + Returns: + The cost function value for the input parameter field x. + ''' + d[:] = dist_norm_to_real(x[:distance_num]) #update distance variables + print("Current distances: ", d) + w_norm[:] = x[distance_num:] #update normalized phase mask parameters + phase_mask[:] = phase_given_w(w_norm) + + compute_phase_factors() #update phase factor matrices + compute_input_field() #update input field based on the new distances + + C1 = _compute_cost_1() + C2 = _compute_cost_2() + C = [-C1, -C2] + opt_history_1.append(C1) + opt_history_2.append(C2) + + if opt_cycle[0] % 10 == 0: + plt.figure() + plt.plot(opt_history_1, 'r', label="Mode #1", markersize=2) + plt.plot(opt_history_2, 'b', label="Mode #2", markersize=2) + plt.xlabel('Iteration') + plt.ylabel('Cost Function Value') + plt.title('Optimization History') + plt.tight_layout() + plt.legend() + plt.savefig("results/modeconv1_optimization_history.pdf") + plt.close() + + opt_cycle[0] = opt_cycle[0] + 1 + + grad = np.zeros((2, distance_num + 4*S)) #initialize gradient array + if opt_cycle[0] < opt_max_eval - 20: + grad[:,:distance_num] = np.zeros(distance_num) #set distance gradient to zero for the first 100 iterations + else: + grad[:,:distance_num] = (dist_grad_1() + dist_grad_2()) * (distance_upper_bound - distance_lower_bound) + #grad[:distance_num] = np.array([0,0,0]) #set distance gradient to zero for now + + grad[0, distance_num:] = -np.concatenate((adjoint_propagate_1() * 2 * np.pi, np.zeros(2*S))) + grad[1, distance_num:] = -np.concatenate((np.zeros(2*S), adjoint_propagate_2() * 2 * np.pi)) + + gc.collect() #force garbage collection to free memory + plt.close('all') + + return C, grad + +def epigraph_constraint(result, epigraph_and_weights, gradient) -> None: + epigraph = epigraph_and_weights[0] + d = epigraph_and_weights[1:1+distance_num] + w_norm = epigraph_and_weights[1+distance_num:] + + ob_vals, grad = cost_fun(np.concatenate((d, w_norm))) + if gradient.size > 0: + gradient[:,0] = -1 + gradient[:,1:] = grad + + result[:] = np.real(ob_vals) - epigraph + +epi_opt_history = [] +def epi_cost_fun(epigraph_and_weights, gradient): + epigraph = epigraph_and_weights[0] + + if gradient.size > 0: + gradient[0] = 1 + gradient[1:] = 0 + + epi_opt_history.append(epigraph) + + return epigraph + +if __name__ == "__main__": + #initialize nlopt solver + epigraph_tolerance = np.array([1e-4]*2) + solver = nlopt.opt(nlopt.LD_CCSAQ, 1 + distance_num + 4*S) + print(1 + distance_num + 4*S) + print(np.shape(epigraph_distance_and_mask_lower_bounds.flatten())) + solver.set_lower_bounds(epigraph_distance_and_mask_lower_bounds.flatten()) + solver.set_upper_bounds(epigraph_distance_and_mask_upper_bounds.flatten()) + solver.set_min_objective(epi_cost_fun) + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-5) + solver.set_param("inner_maxeval", 5) + solver.set_param("verbosity",1) + solver.add_inequality_mconstraint( + lambda result_, epigraph_and_weights_, grad_: epigraph_constraint( + result_, + epigraph_and_weights_, + grad_, + ), + epigraph_tolerance, + ) + + print("Starting optimization...") + start = True + initial_d_norm = np.array([0.4, 0.5, 0.3]) + dist_and_w_norm = np.concatenate((initial_d_norm, w_norm), axis=0) #initialize optimization variables + epigraph_dist_and_w_norm = np.insert(dist_and_w_norm, 0, 0, axis=0) #initialize optimization variables with epigraph + if start: + epigraph_dist_and_w_norm[:] = solver.optimize(epigraph_dist_and_w_norm) + + print("Optimization completed.") + final_epigraph = epigraph_dist_and_w_norm[0] + d[:] = dist_norm_to_real(epigraph_dist_and_w_norm[1:1+distance_num]) #update distance variables + w_norm[:] = epigraph_dist_and_w_norm[1+distance_num:] #update normalized phase mask parameters + print("Optimized distances: ", d) + #verify the results + compute_phase_factors() + compute_input_field() + phase_mask[:] = phase_given_w(w_norm) + forward_propagate_1() + forward_propagate_2() + output_field1 = intermediate_fields_1[-1] + output_field2 = intermediate_fields_2[-1] + + input_power = np.sum(np.abs(input_field)**2) + output_power = np.sum(np.abs(output_field1)**2) + + print("Norm. input power: %.4e" % input_power) + print("Norm. output power: %.4e" % output_power) + + #save the output field for later use + save_output_field = False + if save_output_field: + np.savetxt("results/optimized_output_field.txt", output_field1.reshape((Nx, Ny))) + # Plot results in individual PDF figures + + # Reshape fields to 2D for visualization + output_field1_2d = output_field1.reshape((Nx, Ny)) + output_field2_2d = output_field2.reshape((Nx, Ny)) + phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(w_norm[S:2*S]).reshape((Nx, Ny)) + phase_mask_3 = phase_given_w(w_norm[2*S:3*S]).reshape((Nx, Ny)) + phase_mask_4 = phase_given_w(w_norm[3*S:4*S]).reshape((Nx, Ny)) + + phase_mask_zoom = 150 + make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_3, choose_quantity="phase_mask", save_name="phase_mask_3", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_4, choose_quantity="phase_mask", save_name="phase_mask_4", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + + # Plot: Output field amplitude and phase + make_polarPlot_of(output_field1_2d, choose_quantity="amplitude", save_name="optimized_output1_field_amplitude", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field1_2d, choose_quantity="phase", save_name="optimized_output1_field_phase", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field2_2d, choose_quantity="amplitude", save_name="optimized_output2_field_amplitude", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field2_2d, choose_quantity="phase", save_name="optimized_output2_field_phase", plot_zoom_r=zoom_output_fiber_radius) + + #compute final overlap value + overlap1 = np.abs(np.sum(output_field1.flatten() * np.conj(target1_Efield))) ** 2 + overlap2 = np.abs(np.sum(output_field2.flatten() * np.conj(target2_Efield))) ** 2 + print("Final overlap with target mode 1: %.4e" % overlap1) + print("Final overlap with target mode 2: %.4e" % overlap2) + + slice_y_index = Ny//2 + slice_x = xs * 1e6 + slice_target = target1_Efield.reshape((Nx, Ny))[slice_y_index,:] + slice_output = output_field1.reshape((Nx, Ny))[slice_y_index,:] + #make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude") + #make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase") + + #save the phase masks for later use + savePhase_masks = False + if savePhase_masks: + np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1) + np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2) + + print("All plots saved to results/ folder.") \ No newline at end of file diff --git a/metaprop_fibermode_doublet_ego.py b/metaprop_fibermode_doublet_ego.py new file mode 100644 index 0000000..b5fd177 --- /dev/null +++ b/metaprop_fibermode_doublet_ego.py @@ -0,0 +1,718 @@ +import time +import multiprocessing + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +from smt.applications.ego import EGO, KRG +from smt.sampling_methods import LHS +from smt.design_space import ( + DesignSpace, +) + + +import os +results_folder = "results_ego" +if not os.path.exists(results_folder): + os.makedirs(results_folder) + +############################## +#physics and simulation domain +wavelength = 1.55e-6 +n0 = 1.45 +n1 = 1.0 +n2 = 1.45 + +lda0 = wavelength / n0 +lda1 = wavelength / n1 +lda2 = wavelength / n2 + +k0 = 2 * np.pi / lda0 +k1 = 2 * np.pi / lda1 +k2 = 2 * np.pi / lda2 + +opt_max_eval = 150 + +unit_cell_pitch = 700e-9 #equivalent to spatial sampling + +Nx = 1024 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d0 = 0 #propagation distance: source - MS1 +d1 = 0 #propagation distance: MS1 - MS2 +d2 = d0 #propagation distance: MS2 - target +d = np.zeros((3,)) + +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#Source +beam_waist = 5.2e-6 +#output fibre +output_fiber_core_diameter = 19e-6 +############################## + +############################## +#plan pyfftw objects for forward and inverse FFTs +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = k0 * d[0] * np.sqrt(1 - (lda0 * nu_parallel) ** 2 + 0*1j) +P_0 = np.exp(1j * phase_factor_0) +P_0_nat = np.fft.ifftshift(P_0) +phase_factor_1 = k1 * d[1] * np.sqrt(1 - (lda1 * nu_parallel) ** 2 + 0*1j) +P_1 = np.exp(1j * phase_factor_1) +P_1_nat = np.fft.ifftshift(P_1) +phase_factor_2 = k2 * d[2] * np.sqrt(1 - (lda2 * nu_parallel) ** 2 + 0*1j) +P_2 = np.exp(1j * phase_factor_2) +P_2_nat = np.fft.ifftshift(P_2) + +P_1_dagger = P_1.T.conj() +P_2_dagger = P_2.T.conj() +P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj() +P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj() +############################## + +############################## +#Plot zoom parameters [um] +zoom_x = 150 +zoom_y = 150 +zoom_output_fiber_radius = 63 +full_view_x = size_x * 1e6 / 2 +full_view_y = size_y * 1e6 / 2 +############################## + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(mode_list:list)->list: + '''Returns the fiber mode pattern for the selected n index. + Args: + mode_list: the list of the selected fiber mode indices + Returns: + The fiber mode patterns corresponding to the selected indices returned + as a list of 2D arrays with the same order as the input mode_list + ''' + #check if the modes in mode_list have already been compute by checking + #the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files. + fiber_modes_input_folder = "fiber_modes" + output_patterns = [] + try: + for enum_index, n in enumerate(mode_list): + #try to load mode index n + try: + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" + pattern = np.load(pattern_file) + output_patterns.append(pattern) + print("Loaded fiber patterns for mode index: ", n) + #if mode index n was not found, compute it + except FileNotFoundError: + if not os.path.exists(fiber_modes_input_folder): + os.makedirs(fiber_modes_input_folder) + + print("Computing fiber patterns for mode index: ", n) + lda = 1.55e-6 + + n_co = 1.4630 #core refractive index + n_cl = 1.4585 #cladding refractive index + + fiber_core_diameter = output_fiber_core_diameter # [m] + a = fiber_core_diameter / 2 #fiber core radius + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + + b_solutions_at_V_obs = [] + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + b_mode_sel = b_solutions_at_V_obs[0] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi) + else:#outside core + E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi) + #save the computed pattern to a file for future use + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" + np.save(pattern_file, E_field) + output_patterns.append(E_field) + return output_patterns + except: + raise(Exception("Error in computing fiber patterns")) + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the simulation spatial grid by a given physical (true space) amount. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X + shift_amount_x + Yp = Y + shift_amount_y + return Xp, Yp + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates. + Normalization consists in scaling the field such that its integrated intensity is 1. + + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The 2D normalized Gaussian source field. + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + + return gaussian_field_normalized + +def propagate_source(source_to_propagate): + '''Returns the propagated source field (flattened) just before metasurface 1 (MS1). + This involves propagating the source for a distance d0: from its plane of definition up to MS1. + Args: + source_to_propagate: the source field to be propagated (flattened) + Returns: + The propagated source field (flattened) just before MS1. + ''' + + fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten() + + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + plt.plot(x_axis[i], y_axis[i]) + plt.xlabel("x [um]") + plt.tight_layout() + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.savefig(results_folder + "/" + save_name + ".pdf") + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig(results_folder + "/" + save_name + ".pdf") + plt.close() + +def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None: + '''Makes a polar plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_r: zoom level for the radial axis (in micrometers) + Returns: + None + ''' + fig, ax = plt.subplots(figsize=(6, 6)) + if choose_quantity == "amplitude": + given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting + im = ax.imshow(given_field_2d,origin='lower') + elif choose_quantity == "phase": + given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting + im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + center_x, center_y = Nx//2, Ny//2 + radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels + circle = patches.Circle((center_x, center_y), radius, transform=ax.transData) + im.set_clip_path(circle) + ax.set_xlim(center_x - radius, center_x + radius) + ax.set_ylim(center_y - radius, center_y + radius) + ax.axis('off') + cbar = plt.colorbar(im, ax=ax) + plt.savefig(results_folder + "/" + save_name + ".pdf") +########################### +#initialize source field +source_field = get_source_field(beam_waist) +source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#source field integrated intensity +source_power = np.sum(np.abs(source_field)**2) +print("Source field integrated intensity: %.4e" % source_power) +#make a plot and save the source field +make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +########################### + +########################### +#initialize input field (propagated source field) +input_field = propagate_source(source_field) +input_field_2d = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#save the input field to numpy array +np.save(results_folder + "/input_field.npy", input_field_2d) +#input field integrated intensity +input_field_intensity = np.sum(np.abs(input_field)**2) +print("Input field integrated intensity: %.4e" % input_field_intensity) +#make a plot and save the input field +make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +########################### + +########################### +#initialize target field +target_mode_indices = [2] +fiber_mode_pattern = get_fiber_mode_pattern(target_mode_indices) +target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2)) +target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1) + +# Plot: Target field +make_polarPlot_of(target_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius) +########################### + +########################### +#initialize the normalized phase masks and global phase mask +w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks +X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny)) +rho = np.sqrt(X**2 + Y**2) +circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0) +#w_norm[:S] += circular_mask_1 +target_pattern_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi +w_norm[S:] += target_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 +phase_mask = phase_given_w(w_norm) + +#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. +#the bounding box is the outer shell of a square with thickness 100 um +norm_phase_min = 0 +norm_phase_max = 1 +mask_buffer = 100e-6 +mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) +mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) +mask_lower_bounds = np.zeros_like(mask_upper_bounds) +########################### + +intermediate_fields = [None, None] +def forward_propagate() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy()) + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2)) + + fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny)))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy()) + + end_time = time.time() + #make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + +def adjoint_propagate(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger) + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + +def _compute_cost(): + forward_propagate() + + output_field = intermediate_fields[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field * np.conj(target_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +iter_num = [0] +opt_history = [] +def cost_fun(x, grad): + ''' Cost function to be minimized. + Args: + x: input normalized parameter field (flattened) + grad: gradient of the cost function with respect to x, modified in place (flattened) + Returns: + The cost function value for the input parameter field x. + ''' + w_norm[:] = x + + phase_mask[:] = phase_given_w(w_norm) + + C = _compute_cost() + + opt_history.append(C) + iter_num[0] += 1 + if iter_num[0] % 10 == 0: + plt.figure() + plt.plot(opt_history) + plt.xlabel('Iteration') + plt.ylabel('Cost Function Value') + plt.title('Optimization History') + plt.tight_layout() + plt.savefig(results_folder + "/modeconv1_optimization_history.pdf") + plt.close() + + if grad.size > 0: + grad[:] = adjoint_propagate() * 2 * np.pi + return C + +def metaprop(): + #initialize nlopt solver + solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S) + + solver.set_lower_bounds(mask_lower_bounds.flatten()) + solver.set_upper_bounds(mask_upper_bounds.flatten()) + solver.set_max_objective(cost_fun) + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-7) + solver.set_param("verbosity",1) + + print("Starting optimization...") + start = True + if start: + w_norm[:] = solver.optimize(w_norm) + print("Optimization completed.") + + #verify the results + phase_mask = phase_given_w(w_norm) + forward_propagate() + field_input_ms2 = intermediate_fields[0] + output_field = intermediate_fields[-1] + + input_power = np.sum(np.abs(input_field)**2) + output_power = np.sum(np.abs(output_field)**2) + + print("Norm. input power: %.4e" % input_power) + print("Norm. output power: %.4e" % output_power) + + #save the output field for later use + save_output_field = True + if save_output_field: + np.save(results_folder + "/intermediate_field_0.npy", intermediate_fields[0].reshape((Nx, Ny))) + np.save(results_folder + "/optimized_output_field.npy", output_field.reshape((Nx, Ny))) + # Plot results in individual PDF figures + + # Reshape fields to 2D for visualization + output_field_2d = output_field.reshape((Nx, Ny)) + phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(w_norm[S:]).reshape((Nx, Ny)) + + phase_mask_zoom = 150 + make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + + # Plot: Output field amplitude and phase + make_polarPlot_of(output_field_2d, choose_quantity="amplitude", save_name="optimized_output_field_amplitude", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field_2d, choose_quantity="phase", save_name="optimized_output_field_phase", plot_zoom_r=zoom_output_fiber_radius) + + slice_y_index = Ny//2 + slice_x = xs * 1e6 + slice_target = target_Efield.reshape((Nx, Ny))[slice_y_index,:] + slice_output = output_field.reshape((Nx, Ny))[slice_y_index,:] + make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude") + make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase") + + #save the phase masks for later use + savePhase_masks = True + if savePhase_masks: + np.save(results_folder + "/optimized_phase_mask_1.npy", phase_mask_1) + np.save(results_folder + "/optimized_phase_mask_2.npy", phase_mask_2) + + last_optimum_value = solver.last_optimum_value() + print("Last optimum value: %.4e" % last_optimum_value) + print(f"All plots saved to {results_folder}/ folder.") + return last_optimum_value + +def update_phase_factors(): + phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (lda0 * nu_parallel) ** 2 + 0*1j) + P_0[:] = np.exp(1j * phase_factor_0) + P_0_nat[:] = np.fft.ifftshift(P_0) + phase_factor_1[:] = k1 * d[1] * np.sqrt(1 - (lda1 * nu_parallel) ** 2 + 0*1j) + P_1[:] = np.exp(1j * phase_factor_1) + P_1_nat[:] = np.fft.ifftshift(P_1) + phase_factor_2[:] = k2 * d[2] * np.sqrt(1 - (lda2 * nu_parallel) ** 2 + 0*1j) + P_2[:] = np.exp(1j * phase_factor_2) + P_2_nat[:] = np.fft.ifftshift(P_2) + + P_1_dagger[:] = P_1.T.conj() + P_2_dagger[:] = P_2.T.conj() + P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj() + P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj() + +def update_input_fields(): + input_field[:] = propagate_source(source_field) + input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting + #save the input field to numpy array + #np.save("results/input_field.npy", input_field_2d) + #input field integrated intensity + input_field_intensity = np.sum(np.abs(input_field)**2) + print("Input field integrated intensity: %.4e" % input_field_intensity) + #make a plot and save the input field + make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) + make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) + +def initialize_phase_masks(): + #initialize the normalized phase masks and global phase mask + w_norm[:] = np.zeros((2*S)) #concatenated normalized parameters for both masks + X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny)) + rho = np.sqrt(X**2 + Y**2) + circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0) + #w_norm[:S] += circular_mask_1 + target_pattern_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi + w_norm[S:] += target_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 + phase_mask[:] = phase_given_w(w_norm) + +def reset_globals(): + opt_history[:] = [] + w_norm[:] = np.zeros((2*S)) + phase_mask[:] = phase_given_w(w_norm) + d[:] = np.zeros((3,)) + +def metaprop_update(x): + '''Objective function called from EGO loop. Given a set of input design parameters x, + it updates the system and calls the inner adjoint-based optimization. + Changing the system distance parameters requires updating the following terms: + 1) propagation matrices + 2) input field''' + + if not os.path.exists(f'{results_folder}/metaprop_results.txt'): + with open(f'{results_folder}/metaprop_results.txt', 'w') as f: + f.write("d1 [m] d2 [m] d3 [m] ob1 ob2 t\n") + + m, n = x.shape + C = np.zeros((m,1)) + for i in range(m): + for j in range(n): + d[j] = 1e-6*np.round(x[i,j]) #update the global propagation distances with the new design parameters + + d[-1] = d[0] + assert np.all(d), "Propagation distances must be real-positive." + print("Current propagation distances: ", d) + update_phase_factors() #update the phase factors for the new propagation distances + update_input_fields() + initialize_phase_masks() + print("USING D=",d) + last_optimum_value = metaprop() #solve the inner optimization problem for the given design parameters + + #save all the partial results for a specific set of design parameters x to a text file + #in append mode, so that we can keep track of the optimization history + with open(f'{results_folder}/metaprop_results.txt', 'a') as f: + f.write(f"{d[0]:.6e} {d[1]:.6e} {d[2]:.6e} {last_optimum_value:.6e}\n") + + print("Current cost function value: ", opt_history[-1]) + C[i] = -last_optimum_value #remember ego does minimization + + reset_globals() + return C + +def run_ego(): + ############################## + #EGO setup + ############################## + n_ego_iter = 30 + xlimits = np.array([[100,700], [100,700]]) + seed = 777 + design_space = DesignSpace(xlimits, seed=seed) + criterion = 'EI' + ndoe = 20 + + #build DOE + sampling = LHS(xlimits=xlimits, seed=seed) + xdoe = sampling(ndoe) + + sm = KRG(design_space=design_space, n_start=25, print_global=False) + ego = EGO( + n_iter=n_ego_iter, + criterion=criterion, + xdoe=xdoe, + surrogate=sm, + n_start=25, + ) + + x_opt, y_opt, ind_best, x_data, y_data = ego.optimize(fun=metaprop_update) + #save the optimization results to a text file + np.savetxt(f'{results_folder}/ego_opt_results.txt', np.hstack((x_opt,y_opt))) + +if __name__ == "__main__": + run_ego() \ No newline at end of file diff --git a/metaprop_fibermode_doublet_sym.py b/metaprop_fibermode_doublet_sym.py new file mode 100644 index 0000000..44c6186 --- /dev/null +++ b/metaprop_fibermode_doublet_sym.py @@ -0,0 +1,667 @@ +import time +import multiprocessing + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +import os + +save_folder = "results_sym" +if not os.path.exists(save_folder): + os.makedirs(save_folder) + + + +############################## +#physics and simulation domain +wavelength = 1.55e-6 +n0 = 1.45 +n1 = 1.0 +n2 = 1.45 + +lda0 = wavelength / n0 +lda1 = wavelength / n1 +lda2 = wavelength / n2 + +k0 = 2 * np.pi / lda0 +k1 = 2 * np.pi / lda1 +k2 = 2 * np.pi / lda2 + +opt_max_eval = 150 + +unit_cell_pitch = 700e-9 #equivalent to spatial sampling + +Nx = 1024 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d0 = 403e-6 #propagation distance: source - MS1 +d1 = 158e-6 #propagation distance: MS1 - MS2 +d2 = d0 #propagation distance: MS2 - target + +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#Source +beam_waist = 5.2e-6 +#output fibre +output_fiber_core_diameter = 19e-6 +############################## + +############################## +#plan pyfftw objects for forward and inverse FFTs +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = k0 * d0 * np.sqrt(1 - (lda0 * nu_parallel) ** 2 + 0*1j) +P_0 = np.exp(1j * phase_factor_0) +P_0_nat = np.fft.ifftshift(P_0) +phase_factor_1 = k1 * d1 * np.sqrt(1 - (lda1 * nu_parallel) ** 2 + 0*1j) +P_1 = np.exp(1j * phase_factor_1) +P_1_nat = np.fft.ifftshift(P_1) +phase_factor_2 = k2 * d2 * np.sqrt(1 - (lda2 * nu_parallel) ** 2 + 0*1j) +P_2 = np.exp(1j * phase_factor_2) +P_2_nat = np.fft.ifftshift(P_2) + +P_1_dagger = P_1.T.conj() +P_2_dagger = P_2.T.conj() +P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj() +P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj() +############################## + +############################## +#Plot zoom parameters [um] +zoom_x = 150 +zoom_y = 150 +zoom_output_fiber_radius = 63 +full_view_x = size_x * 1e6 / 2 +full_view_y = size_y * 1e6 / 2 +############################## + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(mode_list:list)->list: + '''Returns the fiber mode pattern for the selected n index. + Args: + mode_list: the list of the selected fiber mode indices + Returns: + The fiber mode patterns corresponding to the selected indices returned + as a list of 2D arrays with the same order as the input mode_list + ''' + #check if the modes in mode_list have already been compute by checking + #the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files. + fiber_modes_input_folder = "fiber_modes" + output_patterns = [] + try: + for enum_index, n in enumerate(mode_list): + #try to load mode index n + try: + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" + pattern = np.load(pattern_file) + output_patterns.append(pattern) + print("Loaded fiber patterns for mode index: ", n) + #if mode index n was not found, compute it + except FileNotFoundError: + if not os.path.exists(fiber_modes_input_folder): + os.makedirs(fiber_modes_input_folder) + + print("Computing fiber patterns for mode index: ", n) + lda = 1.55e-6 + + n_co = 1.4630 #core refractive index + n_cl = 1.4585 #cladding refractive index + + fiber_core_diameter = output_fiber_core_diameter # [m] + a = fiber_core_diameter / 2 #fiber core radius + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + + b_solutions_at_V_obs = [] + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + b_mode_sel = b_solutions_at_V_obs[0] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi) + else:#outside core + E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi) + #save the computed pattern to a file for future use + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" + np.save(pattern_file, E_field) + output_patterns.append(E_field) + return output_patterns + except: + raise(Exception("Error in computing fiber patterns")) + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the simulation spatial grid by a given physical (true space) amount. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X + shift_amount_x + Yp = Y + shift_amount_y + return Xp, Yp + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates. + Normalization consists in scaling the field such that its integrated intensity is 1. + + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The 2D normalized Gaussian source field. + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + + return gaussian_field_normalized + +def propagate_source(source_to_propagate): + '''Returns the propagated source field (flattened) just before metasurface 1 (MS1). + This involves propagating the source for a distance d0: from its plane of definition up to MS1. + Args: + source_to_propagate: the source field to be propagated (flattened) + Returns: + The propagated source field (flattened) just before MS1. + ''' + + fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten() + + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + plt.plot(x_axis[i], y_axis[i]) + plt.xlabel("x [um]") + plt.tight_layout() + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.savefig(f"{save_folder}/" + save_name + ".pdf") + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig(f"{save_folder}/" + save_name + ".pdf") + plt.close() + +def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None: + '''Makes a polar plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_r: zoom level for the radial axis (in micrometers) + Returns: + None + ''' + fig, ax = plt.subplots(figsize=(6, 6)) + if choose_quantity == "amplitude": + given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting + im = ax.imshow(given_field_2d,origin='lower') + elif choose_quantity == "phase": + given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting + im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + center_x, center_y = Nx//2, Ny//2 + radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels + circle = patches.Circle((center_x, center_y), radius, transform=ax.transData) + im.set_clip_path(circle) + ax.set_xlim(center_x - radius, center_x + radius) + ax.set_ylim(center_y - radius, center_y + radius) + ax.axis('off') + cbar = plt.colorbar(im, ax=ax) + plt.savefig(f"{save_folder}/" + save_name + ".pdf") + +def symmetrize(w, nx, ny): + ''' + Takes an input array, turns it into a 2D array, and symmetrizes it along the x and y axes. + Returns the first quadrant of shape (nx,ny), flattened + ''' + try: + w_2d = w.reshape((Nx, Ny)) + except ValueError: + raise ValueError(f"Input array w cannot be reshaped to ({Nx}, {Ny}). Current shape: {w.shape}") + w_symm = np.zeros((nx, ny)) + w_symm[:,:] = w_2d[:nx, :ny] + return w_symm.flatten() + +def expand_symmetrize(w_symm, nx, ny): + ''' + Takes a symmetrized array of shape (nx, ny) and expands it to a full 2D array of shape (Nx, Ny) by mirroring, flattened. + ''' + w_full = np.zeros((Nx, Ny)) + w_full[:nx, :ny] = w_symm.reshape((nx, ny)) + w_full[:nx, ny:] = np.flip(w_symm.reshape((nx, ny)), axis=1) + w_full[nx:, :ny] = np.flip(w_symm.reshape((nx, ny)), axis=0) + w_full[nx:, ny:] = np.flip(np.flip(w_symm.reshape((nx, ny)), axis=0), axis=1) + return w_full.flatten() + +########################### +#initialize source field +source_field = get_source_field(beam_waist) +source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#source field integrated intensity +source_power = np.sum(np.abs(source_field)**2) +print("Source field integrated intensity: %.4e" % source_power) +#make a plot and save the source field +make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +########################### + +########################### +#initialize input field (propagated source field) +input_field = propagate_source(source_field) +input_field_2d = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#save the input field to numpy array +np.save(f"{save_folder}/input_field.npy", input_field_2d) +#input field integrated intensity +input_field_intensity = np.sum(np.abs(input_field)**2) +print("Input field integrated intensity: %.4e" % input_field_intensity) +#make a plot and save the input field +make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +########################### + +########################### +#initialize target field +target_mode_indices = [2] +fiber_mode_pattern = get_fiber_mode_pattern(target_mode_indices) +target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2)) +target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1) + +# Plot: Target field +make_polarPlot_of(target_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius) +########################### + +########################### +#initialize the normalized phase masks and global phase mask +w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks +X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny)) +rho = np.sqrt(X**2 + Y**2) +circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0) +#w_norm[:S] += circular_mask_1 +target_pattern_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi +w_norm[S:] += target_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 + +w_norm_symm_1 = symmetrize(w_norm[:S], Nx//2, Ny//2) +w_norm_symm_2 = symmetrize(w_norm[S:], Nx//2, Ny//2) + +w_norm_symm = np.concatenate((w_norm_symm_1, w_norm_symm_2), axis=0) + +phase_mask = phase_given_w(w_norm) + +#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. +#the bounding box is the outer shell of a square with thickness 100 um +norm_phase_min = 0 +norm_phase_max = 1 +mask_buffer = 100e-6 +mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) +mask_upper_bounds_symm = mask_upper_bounds.reshape((Nx,Ny))[:Nx//2, :Ny//2].flatten() + +mask_upper_bounds_symm = np.concatenate((mask_upper_bounds_symm, mask_upper_bounds_symm),axis=0) +mask_lower_bounds_symm = np.zeros_like(mask_upper_bounds_symm) + +########################### + +intermediate_fields = [None, None] +def forward_propagate() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy()) + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2)) + + fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny)))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy()) + + end_time = time.time() + make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + +def reduce_symmetrized_gradient(g_full, nx, ny): + g_2d = g_full.reshape((Nx, Ny)) + + g_reduced = ( + g_2d[:nx, :ny] + + np.flip(g_2d[:nx, ny:], axis=1) + + np.flip(g_2d[nx:, :ny], axis=0) + + np.flip(np.flip(g_2d[nx:, ny:], axis=0), axis=1) + ) + + return g_reduced.flatten() + +def adjoint_propagate(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger) + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return grad_C_phi_1, grad_C_phi_2 + +def _compute_cost(): + + forward_propagate() + + output_field = intermediate_fields[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field * np.conj(target_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +opt_history = [] +iter_num = [0] +def cost_fun(x, grad): + ''' Cost function to be minimized. + Args: + x: input normalized parameter field (flattened) + grad: gradient of the cost function with respect to x, modified in place (flattened) + Returns: + The cost function value for the input parameter field x. + ''' + #since the objective function is to maximize the overlap, and the target mode is C4 symmetric, + #we can let the algorithm optimize only a fourth of the phase mask and then synthetize automatically the remaining three quadrants. + #Since we have two metasurfaces, the total number of parameters the algorithm needs to optimize is S/2 (S/4 for each metasurface) + ms1_first_quadrant = x[:S//4].reshape(Nx//2, Ny//2) + ms2_first_quadrant = x[S//4:].reshape(Nx//2, Ny//2) + + ms1_first_second = np.concatenate((ms1_first_quadrant, np.flip(ms1_first_quadrant, axis=1)), axis=1) + ms1 = np.concatenate((ms1_first_second, np.flip(ms1_first_second, axis=0)), axis=0) + + ms2_first_second = np.concatenate((ms2_first_quadrant, np.flip(ms2_first_quadrant, axis=1)), axis=1) + ms2 = np.concatenate((ms2_first_second, np.flip(ms2_first_second, axis=0)), axis=0) + + all_ms = np.concatenate((ms1.flatten(), ms2.flatten())) + + w_norm[:] = all_ms + + phase_mask[:] = phase_given_w(w_norm) + + + C = _compute_cost() + + opt_history.append(C) + iter_num[0] += 1 + if iter_num[0] % 10 == 0: + plt.figure() + plt.plot(opt_history) + plt.xlabel('Iteration') + plt.ylabel('Cost Function Value') + plt.title('Optimization History') + plt.tight_layout() + plt.savefig(f"{save_folder}/optimization_history.pdf") + plt.close() + + if grad.size > 0: + full_grad1, full_grad2 = adjoint_propagate() + reduced_grad1 = reduce_symmetrized_gradient(full_grad1, Nx//2, Ny//2) + reduced_grad2 = reduce_symmetrized_gradient(full_grad2, Nx//2, Ny//2) + reduced_grads = np.concatenate((reduced_grad1, reduced_grad2), axis=0) + grad[:] = reduced_grads * 2 * np.pi + return C + +if __name__ == "__main__": + #initialize nlopt solver + solver = nlopt.opt(nlopt.LD_CCSAQ, S//2) + + solver.set_lower_bounds(mask_lower_bounds_symm.flatten()) + solver.set_upper_bounds(mask_upper_bounds_symm.flatten()) + solver.set_max_objective(cost_fun) + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-7) + solver.set_param("verbosity",1) + + print("Starting optimization...") + + start = True + if start: + w_norm_symm[:] = solver.optimize(w_norm_symm) + w_norm[:S] = expand_symmetrize(w_norm_symm[:S//4], Nx//2, Ny//2) + w_norm[S:] = expand_symmetrize(w_norm_symm[S//4:], Nx//2, Ny//2) + + print("Optimization completed.") + print("LAST OPTIMUM VALUE: ", solver.last_optimum_value()) + #verify the results + phase_mask = phase_given_w(w_norm) + forward_propagate() + field_input_ms2 = intermediate_fields[0] + output_field = intermediate_fields[-1] + + input_power = np.sum(np.abs(input_field)**2) + output_power = np.sum(np.abs(output_field)**2) + + print("Norm. input power: %.4e" % input_power) + print("Norm. output power: %.4e" % output_power) + + #save the output field for later use + save_output_field = True + if save_output_field: + np.save(f"{save_folder}/intermediate_field_0.npy", intermediate_fields[0].reshape((Nx, Ny))) + np.save(f"{save_folder}/optimized_output_field.npy", output_field.reshape((Nx, Ny))) + # Plot results in individual PDF figures + + # Reshape fields to 2D for visualization + output_field_2d = output_field.reshape((Nx, Ny)) + phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(w_norm[S:]).reshape((Nx, Ny)) + + phase_mask_zoom = 150 + make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + + # Plot: Output field amplitude and phase + make_polarPlot_of(output_field_2d, choose_quantity="amplitude", save_name="optimized_output_field_amplitude", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field_2d, choose_quantity="phase", save_name="optimized_output_field_phase", plot_zoom_r=zoom_output_fiber_radius) + + slice_y_index = Ny//2 + slice_x = xs * 1e6 + slice_target = target_Efield.reshape((Nx, Ny))[slice_y_index,:] + slice_output = output_field.reshape((Nx, Ny))[slice_y_index,:] + make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude") + make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase") + + #save the phase masks for later use + savePhase_masks = True + if savePhase_masks: + np.save(f"{save_folder}/optimized_phase_mask_1.npy", phase_mask_1) + np.save(f"{save_folder}/optimized_phase_mask_2.npy", phase_mask_2) + + print(f"All plots saved to {save_folder}/ folder.") \ No newline at end of file diff --git a/metaprop_multi_objective/metaprop_fibermode_doublet_distGrad.py b/metaprop_multi_objective/metaprop_fibermode_doublet_distGrad.py new file mode 100644 index 0000000..85a88d7 --- /dev/null +++ b/metaprop_multi_objective/metaprop_fibermode_doublet_distGrad.py @@ -0,0 +1,717 @@ +import time +import multiprocessing + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +import os + +if not os.path.exists("results"): + os.makedirs("results") + +############################## +#physics and simulation domain +wavelength = 1.55e-6 +n0 = 1.0 +n1 = 1.0 +n2 = 1.0 + +lda0 = wavelength / n0 +lda1 = wavelength / n1 +lda2 = wavelength / n2 + +k0 = 2 * np.pi / lda0 +k1 = 2 * np.pi / lda1 +k2 = 2 * np.pi / lda2 + +opt_max_eval = 150 + +unit_cell_pitch = 700e-9 #equivalent to spatial sampling + +Nx = 1024 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d0_init = 300e-6 #propagation distance: source - MS1 +d1_init = 500e-6 #propagation distance: MS1 - MS2 +d2_init = 400e-6 #propagation distance: MS2 - target +distance_lower_bound = 100e-6 +distance_upper_bound = 1000e-6 +distance_num = 3 +d = np.array([d0_init, d1_init, d2_init]) + +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#Source +beam_waist = 5.2e-6 +#output fibre +output_fiber_core_diameter = 19e-6 +############################## + +############################## +#plan pyfftw objects for forward and inverse FFTs +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = np.zeros((Nx, Ny), dtype=complex) +P_0 = np.zeros((Nx, Ny), dtype=complex) +P_0_nat = np.zeros((Nx, Ny), dtype=complex) +phase_factor_1 = np.zeros((Nx, Ny), dtype=complex) +P_1 = np.zeros((Nx, Ny), dtype=complex) +P_1_nat = np.zeros((Nx, Ny), dtype=complex) +phase_factor_2 = np.zeros((Nx, Ny), dtype=complex) +P_2 = np.zeros((Nx, Ny), dtype=complex) +P_2_nat = np.zeros((Nx, Ny), dtype=complex) +P_1_dagger = np.zeros((Nx, Ny), dtype=complex) +P_2_dagger = np.zeros((Nx, Ny), dtype=complex) +P_1_dagger_nat = np.zeros((Nx, Ny), dtype=complex) +P_2_dagger_nat = np.zeros((Nx, Ny), dtype=complex) + +############################## +def compute_phase_factors(): + phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_0[:] = np.exp(1j * phase_factor_0) + P_0_nat[:] = np.fft.ifftshift(P_0) + phase_factor_1[:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_1[:] = np.exp(1j * phase_factor_1) + P_1_nat[:] = np.fft.ifftshift(P_1) + phase_factor_2[:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_2[:] = np.exp(1j * phase_factor_2) + P_2_nat[:] = np.fft.ifftshift(P_2) + + P_1_dagger[:] = P_1.T.conj() + P_2_dagger[:] = P_2.T.conj() + P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj() + P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj() + + +############################## +#Plot zoom parameters [um] +#Plot zoom parameters [um] +zoom_x = 150 +zoom_y = 150 +zoom_output_fiber_radius = 63 +full_view_x = size_x * 1e6 / 2 +full_view_y = size_y * 1e6 / 2 +############################## + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(mode_list:list)->list: + '''Returns the fiber mode pattern for the selected n index. + Args: + mode_list: the list of the selected fiber mode indices + Returns: + The fiber mode patterns corresponding to the selected indices returned + as a list of 2D arrays with the same order as the input mode_list + ''' + #check if the modes in mode_list have already been compute by checking + #the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files. + fiber_modes_input_folder = "fiber_modes" + output_patterns = [] + try: + for enum_index, n in enumerate(mode_list): + #try to load mode index n + try: + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" + pattern = np.load(pattern_file) + output_patterns.append(pattern) + print("Loaded fiber patterns for mode index: ", n) + #if mode index n was not found, compute it + except FileNotFoundError: + if not os.path.exists(fiber_modes_input_folder): + os.makedirs(fiber_modes_input_folder) + + print("Computing fiber patterns for mode index: ", n) + lda = 1.55e-6 + + n_co = 1.4630 #core refractive index + n_cl = 1.4585 #cladding refractive index + + fiber_core_diameter = output_fiber_core_diameter # [m] + a = fiber_core_diameter / 2 #fiber core radius + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + + b_solutions_at_V_obs = [] + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + b_mode_sel = b_solutions_at_V_obs[0] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi) + else:#outside core + E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi) + #save the computed pattern to a file for future use + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" + np.save(pattern_file, E_field) + output_patterns.append(E_field) + return output_patterns + except: + raise(Exception("Error in computing fiber patterns")) + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the simulation spatial grid by a given physical (true space) amount. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X + shift_amount_x + Yp = Y + shift_amount_y + return Xp, Yp + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates. + Normalization consists in scaling the field such that its integrated intensity is 1. + + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The 2D normalized Gaussian source field. + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + + return gaussian_field_normalized + +def propagate_source(source_to_propagate): + '''Returns the propagated source field (flattened) just before metasurface 1 (MS1). + This involves propagating the source for a distance d0: from its plane of definition up to MS1. + Args: + source_to_propagate: the source field to be propagated (flattened) + Returns: + The propagated source field (flattened) just before MS1. + ''' + + fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten() + + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + plt.plot(x_axis[i], y_axis[i]) + plt.xlabel("x [um]") + plt.tight_layout() + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None: + '''Makes a polar plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_r: zoom level for the radial axis (in micrometers) + Returns: + None + ''' + fig, ax = plt.subplots(figsize=(6, 6)) + if choose_quantity == "amplitude": + given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting + im = ax.imshow(given_field_2d,origin='lower') + elif choose_quantity == "phase": + given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting + im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + center_x, center_y = Nx//2, Ny//2 + radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels + circle = patches.Circle((center_x, center_y), radius, transform=ax.transData) + im.set_clip_path(circle) + ax.set_xlim(center_x - radius, center_x + radius) + ax.set_ylim(center_y - radius, center_y + radius) + ax.axis('off') + cbar = plt.colorbar(im, ax=ax) + plt.savefig("results/" + save_name + ".pdf") + +########################### +#initialize source field +source_field = get_source_field(beam_waist) +source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#source field integrated intensity +source_power = np.sum(np.abs(source_field)**2) +print("Source field integrated intensity: %.4e" % source_power) +#make a plot and save the source field +make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +########################### + +########################### +#initialize input field (propagated source field) +input_field = np.zeros((S,), dtype=complex) +input_field_2d = np.zeros((Nx, Ny), dtype=complex) + +#make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +#make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +def compute_input_field(): + input_field[:] = propagate_source(source_field) + input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting + #input field integrated intensity + input_field_intensity = np.sum(np.abs(input_field)**2) + print("Input field integrated intensity: %.4e" % input_field_intensity) +########################### + +########################### +#initialize target field +target_mode_indices = [2] +fiber_mode_pattern = get_fiber_mode_pattern(target_mode_indices) +target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2)) +target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1) + +# Plot: Target field +make_polarPlot_of(target_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius) +########################### + +########################### +#initialize the normalized phase masks and global phase mask +w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks +X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny)) +rho = np.sqrt(X**2 + Y**2) +circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0) +#w_norm[:S] += circular_mask_1 +target_pattern_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi +w_norm[S:] += target_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 +phase_mask = phase_given_w(w_norm) + +#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. +#the bounding box is the outer shell of a square with thickness 100 um +norm_phase_min = 0 +norm_phase_max = 1 +mask_buffer = 100e-6 +mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) +mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) +mask_lower_bounds = np.zeros_like(mask_upper_bounds) +distance_upper_bounds = np.array([1, 1, 1]) +distance_lower_bounds = np.array([0, 0, 0]) + +distance_and_mask_upper_bounds = np.concatenate((distance_upper_bounds, mask_upper_bounds),axis=0) +distance_and_mask_lower_bounds = np.concatenate((distance_lower_bounds, mask_lower_bounds),axis=0) +########################### + +intermediate_fields = [None, None] +def forward_propagate() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields[0] = ifft_operator.output_array.copy() + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2)) + + fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:].reshape((Nx, Ny)))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields[1] = ifft_operator.output_array.copy() + + end_time = time.time() + make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + +def adjoint_propagate(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * fft_input_array).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + +def dist_grad(): + P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j) + print("shape of P_0_tilde: ", P_0_tilde.shape) + P_0_triangle = P_0 * P_0_tilde + P_0_triangle_nat = np.fft.ifftshift(P_0_triangle) + + P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j) + P_1_triangle = P_1 * P_1_tilde + P_1_triangle_nat = np.fft.ifftshift(P_1_triangle) + + P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j) + P_2_triangle = P_2 * P_2_tilde + P_2_triangle_nat = np.fft.ifftshift(P_2_triangle) + + Phi_1 = np.exp(1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2 = np.exp(1j * phase_mask[S:]).reshape((Nx, Ny)) + Sf = np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) + S_dagger = np.conj(Sf) + E_t_dagger = np.conj(target_Efield_2d) + + ################################ + #dC_d0 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d1 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d2 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat + ifft_operator() + + dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + + return np.array([dC_d0, dC_d1, dC_d2]) + +def dist_norm_to_real(norm_dist): + '''Maps normalized distance parameters to real distance values. + Args: + norm_dist: normalized distance parameters (flattened) + Returns: + Real distance values corresponding to the normalized parameters (flattened)''' + return norm_dist * (distance_upper_bound - distance_lower_bound) + distance_lower_bound + +def _compute_cost(): + + forward_propagate() + + output_field = intermediate_fields[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field * np.conj(target_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +opt_history = [] +opt_cycle = [0] +def cost_fun(x, grad): + ''' Cost function to be minimized. + Args: + x: input normalized parameter field (flattened) + grad: gradient of the cost function with respect to x, modified in place (flattened) + Returns: + The cost function value for the input parameter field x. + ''' + d[:] = dist_norm_to_real(x[:distance_num]) #update distance variables + print("Current distances: ", d) + w_norm[:] = x[distance_num:] #update normalized phase mask parameters + phase_mask[:] = phase_given_w(w_norm) + + compute_phase_factors() #update phase factor matrices + compute_input_field() #update input field based on the new distances + + C = _compute_cost() + + opt_history.append(C) + + plt.figure() + plt.plot(opt_history) + plt.xlabel('Iteration') + plt.ylabel('Cost Function Value') + plt.title('Optimization History') + plt.tight_layout() + plt.savefig("results/modeconv1_optimization_history.pdf") + plt.close() + + opt_cycle[0] = opt_cycle[0] + 1 + if grad.size > 0: + if opt_cycle[0] < 1000: + grad[:distance_num] = np.zeros(distance_num) #set distance gradient to zero for the first 100 iterations + else: + grad[:distance_num] = dist_grad() * (distance_upper_bound - distance_lower_bound) + #grad[:distance_num] = np.array([0,0,0]) #set distance gradient to zero for now + grad[distance_num:] = adjoint_propagate() * 2 * np.pi + return C + +if __name__ == "__main__": + #initialize nlopt solver + solver = nlopt.opt(nlopt.LD_CCSAQ, distance_num + 2*S) + + solver.set_lower_bounds(distance_and_mask_lower_bounds.flatten()) + solver.set_upper_bounds(distance_and_mask_upper_bounds.flatten()) + solver.set_max_objective(cost_fun) + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-7) + solver.set_param("verbosity",1) + + print("Starting optimization...") + start = True + initial_d = np.array([0.4, 0.5, 0.3]) + dist_and_w_norm = np.concatenate((initial_d, w_norm), axis=0) #initialize optimization variables + + if start: + dist_and_w_norm[:] = solver.optimize(dist_and_w_norm) + + print("Optimization completed.") + d[:] = dist_norm_to_real(dist_and_w_norm[:distance_num]) #update distance variables + w_norm[:] = dist_and_w_norm[distance_num:] #update normalized phase mask parameters + print("Optimized distances: ", d) + #verify the results + compute_phase_factors() + compute_input_field() + phase_mask[:] = phase_given_w(w_norm) + forward_propagate() + output_field = intermediate_fields[-1] + + input_power = np.sum(np.abs(input_field)**2) + output_power = np.sum(np.abs(output_field)**2) + + print("Norm. input power: %.4e" % input_power) + print("Norm. output power: %.4e" % output_power) + + #save the output field for later use + save_output_field = False + if save_output_field: + np.savetxt("results/optimized_output_field.txt", output_field.reshape((Nx, Ny))) + # Plot results in individual PDF figures + + # Reshape fields to 2D for visualization + output_field_2d = output_field.reshape((Nx, Ny)) + phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(w_norm[S:]).reshape((Nx, Ny)) + + phase_mask_zoom = 150 + make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + + # Plot: Output field amplitude and phase + make_polarPlot_of(output_field_2d, choose_quantity="amplitude", save_name="optimized_output_field_amplitude", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field_2d, choose_quantity="phase", save_name="optimized_output_field_phase", plot_zoom_r=zoom_output_fiber_radius) + + slice_y_index = Ny//2 + slice_x = xs * 1e6 + slice_target = target_Efield.reshape((Nx, Ny))[slice_y_index,:] + slice_output = output_field.reshape((Nx, Ny))[slice_y_index,:] + make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude") + make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase") + + #save the phase masks for later use + savePhase_masks = True + if savePhase_masks: + np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1) + np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2) + + print("All plots saved to results/ folder.") \ No newline at end of file diff --git a/metaprop_multi_objective/metaprop_fibermode_doublet_distGrad_xy.py b/metaprop_multi_objective/metaprop_fibermode_doublet_distGrad_xy.py new file mode 100644 index 0000000..e278ab2 --- /dev/null +++ b/metaprop_multi_objective/metaprop_fibermode_doublet_distGrad_xy.py @@ -0,0 +1,901 @@ +import gc +import time +import multiprocessing + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +import os + +if not os.path.exists("results"): + os.makedirs("results") + +############################## +#physics and simulation domain +wavelength = 1.55e-6 +n0 = 1.0 +n1 = 1.0 +n2 = 1.0 + +lda0 = wavelength / n0 +lda1 = wavelength / n1 +lda2 = wavelength / n2 + +k0 = 2 * np.pi / lda0 +k1 = 2 * np.pi / lda1 +k2 = 2 * np.pi / lda2 + +opt_max_eval = 250 + +unit_cell_pitch = 700e-9 #equivalent to spatial sampling + +Nx = 1024 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d0_init = 300e-6 #propagation distance: source - MS1 +d1_init = 500e-6 #propagation distance: MS1 - MS2 +d2_init = 400e-6 #propagation distance: MS2 - target +distance_lower_bound = 100e-6 +distance_upper_bound = 1000e-6 +distance_num = 3 +d = np.array([d0_init, d1_init, d2_init]) + +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#Source +beam_waist = 5.2e-6 +#output fibre +output_fiber_core_diameter = 19e-6 +############################## + +############################## +#plan pyfftw objects for forward and inverse FFTs +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = np.zeros((Nx, Ny), dtype=complex) +P_0 = np.zeros((Nx, Ny), dtype=complex) +P_0_nat = np.zeros((Nx, Ny), dtype=complex) +phase_factor_1 = np.zeros((Nx, Ny), dtype=complex) +P_1 = np.zeros((Nx, Ny), dtype=complex) +P_1_nat = np.zeros((Nx, Ny), dtype=complex) +phase_factor_2 = np.zeros((Nx, Ny), dtype=complex) +P_2 = np.zeros((Nx, Ny), dtype=complex) +P_2_nat = np.zeros((Nx, Ny), dtype=complex) +P_1_dagger = np.zeros((Nx, Ny), dtype=complex) +P_2_dagger = np.zeros((Nx, Ny), dtype=complex) +P_1_dagger_nat = np.zeros((Nx, Ny), dtype=complex) +P_2_dagger_nat = np.zeros((Nx, Ny), dtype=complex) + +############################## +def compute_phase_factors(): + phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_0[:] = np.exp(1j * phase_factor_0) + P_0_nat[:] = np.fft.ifftshift(P_0) + phase_factor_1[:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_1[:] = np.exp(1j * phase_factor_1) + P_1_nat[:] = np.fft.ifftshift(P_1) + phase_factor_2[:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_2[:] = np.exp(1j * phase_factor_2) + P_2_nat[:] = np.fft.ifftshift(P_2) + + P_1_dagger[:] = P_1.T.conj() + P_2_dagger[:] = P_2.T.conj() + P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj() + P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj() + + +############################## +#Plot zoom parameters [um] +#Plot zoom parameters [um] +zoom_x = 150 +zoom_y = 150 +zoom_output_fiber_radius = 25 +full_view_x = size_x * 1e6 / 2 +full_view_y = size_y * 1e6 / 2 +############################## + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(mode_list:list)->list: + '''Returns the fiber mode pattern for the selected n index. + Args: + mode_list: the list of the selected fiber mode indices + Returns: + The fiber mode patterns corresponding to the selected indices returned + as a list of 2D arrays with the same order as the input mode_list + ''' + #check if the modes in mode_list have already been compute by checking + #the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files. + fiber_modes_input_folder = "fiber_modes" + output_patterns = [] + try: + for enum_index, n in enumerate(mode_list): + #try to load mode index n + try: + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" + pattern = np.load(pattern_file) + output_patterns.append(pattern) + print("Loaded fiber patterns for mode index: ", n) + #if mode index n was not found, compute it + except FileNotFoundError: + if not os.path.exists(fiber_modes_input_folder): + os.makedirs(fiber_modes_input_folder) + + print("Computing fiber patterns for mode index: ", n) + lda = 1.55e-6 + + n_co = 1.4630 #core refractive index + n_cl = 1.4585 #cladding refractive index + + fiber_core_diameter = output_fiber_core_diameter # [m] + a = fiber_core_diameter / 2 #fiber core radius + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + + b_solutions_at_V_obs = [] + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + b_mode_sel = b_solutions_at_V_obs[0] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi) + else:#outside core + E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi) + #save the computed pattern to a file for future use + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" + np.save(pattern_file, E_field) + output_patterns.append(E_field) + return output_patterns + except: + raise(Exception("Error in computing fiber patterns")) + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the simulation spatial grid by a given physical (true space) amount. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X + shift_amount_x + Yp = Y + shift_amount_y + return Xp, Yp + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates. + Normalization consists in scaling the field such that its integrated intensity is 1. + + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The 2D normalized Gaussian source field. + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + + return gaussian_field_normalized + +def propagate_source(source_to_propagate): + '''Returns the propagated source field (flattened) just before metasurface 1 (MS1). + This involves propagating the source for a distance d0: from its plane of definition up to MS1. + Args: + source_to_propagate: the source field to be propagated (flattened) + Returns: + The propagated source field (flattened) just before MS1. + ''' + + fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten() + + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + plt.plot(x_axis[i], y_axis[i]) + plt.xlabel("x [um]") + plt.tight_layout() + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None: + '''Makes a polar plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_r: zoom level for the radial axis (in micrometers) + Returns: + None + ''' + fig, ax = plt.subplots(figsize=(6, 6)) + if choose_quantity == "amplitude": + given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting + im = ax.imshow(given_field_2d,origin='lower') + elif choose_quantity == "phase": + given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting + im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + center_x, center_y = Nx//2, Ny//2 + radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels + circle = patches.Circle((center_x, center_y), radius, transform=ax.transData) + im.set_clip_path(circle) + ax.set_xlim(center_x - radius, center_x + radius) + ax.set_ylim(center_y - radius, center_y + radius) + ax.axis('off') + cbar = plt.colorbar(im, ax=ax) + plt.savefig("results/" + save_name + ".pdf") + +########################### +#initialize source field +source_field = get_source_field(beam_waist) +source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#source field integrated intensity +source_power = np.sum(np.abs(source_field)**2) +print("Source field integrated intensity: %.4e" % source_power) +#make a plot and save the source field +make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +########################### + +########################### +#initialize input field (propagated source field) +input_field = np.zeros((S,), dtype=complex) +input_field_2d = np.zeros((Nx, Ny), dtype=complex) + +#make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +#make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +def compute_input_field(): + input_field[:] = propagate_source(source_field) + input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting + #input field integrated intensity + input_field_intensity = np.sum(np.abs(input_field)**2) + print("Input field integrated intensity: %.4e" % input_field_intensity) +########################### + +########################### +#initialize target field +target_mode_indices = [1,2] +fiber_mode_pattern1, fiber_mode_pattern2 = get_fiber_mode_pattern(target_mode_indices) +target1_Efield_2d = fiber_mode_pattern1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern1)**2)) +target2_Efield_2d = fiber_mode_pattern2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern2)**2)) +target1_Efield = target1_Efield_2d.flatten() #normalized target field (intensity = 1) +target2_Efield = target2_Efield_2d.flatten() #normalized target field (intensity = 1) + +# Plot: Target field +make_polarPlot_of(target1_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target1_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target2_Efield_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target2_Efield_2d, choose_quantity="phase", save_name="target_field_2_phase_polar", plot_zoom_r=zoom_output_fiber_radius) +########################### + +########################### +#initialize the normalized phase masks and global phase mask +w_norm = np.zeros((4*S)) #concatenated normalized parameters for both masks +X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny)) +rho = np.sqrt(X**2 + Y**2) +circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0) +#w_norm[:S] += circular_mask_1 +target1_pattern_phase = (np.angle(target1_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi +w_norm[S:2*S] += target1_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 + +target2_pattern_phase = (np.angle(target2_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi +w_norm[3*S:4*S] += target2_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 + +phase_mask = phase_given_w(w_norm) + +#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. +#the bounding box is the outer shell of a square with thickness 100 um +norm_phase_min = 0 +norm_phase_max = 1 +mask_buffer = 100e-6 +mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) +mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) +mask_lower_bounds = np.zeros_like(mask_upper_bounds) +distance_upper_bounds = np.array([1, 1, 1]) +distance_lower_bounds = np.array([0, 0, 0]) + +distance_and_mask_upper_bounds = np.concatenate((distance_upper_bounds, mask_upper_bounds, mask_upper_bounds),axis=0) +distance_and_mask_lower_bounds = np.concatenate((distance_lower_bounds, mask_lower_bounds, mask_lower_bounds),axis=0) +########################### + +intermediate_fields_1 = [None, None] +intermediate_fields_2 = [None, None] +def forward_propagate_1() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields_1[0] = ifft_operator.output_array.copy() + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_1[0])**2)) + + fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:2*S].reshape((Nx, Ny)))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields_1[1] = ifft_operator.output_array.copy() + + end_time = time.time() + make_2Dplot_of(intermediate_fields_1[0], choose_quantity="amplitude", save_name="intermediate_field_1_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(intermediate_fields_1[1], choose_quantity="amplitude", save_name="intermediate_field_1_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + +def forward_propagate_2() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[2*S:3*S])).reshape((Nx, Ny)) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields_2[0] = ifft_operator.output_array.copy() + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_2[0])**2)) + + fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[3*S:4*S].reshape((Nx, Ny)))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields_2[1] = ifft_operator.output_array.copy() + + end_time = time.time() + #make_2Dplot_of(intermediate_fields_2[0], choose_quantity="amplitude", save_name="intermediate_field_2_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(intermediate_fields_2[1], choose_quantity="amplitude", save_name="intermediate_field_2_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + +def adjoint_propagate_1(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:2*S]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d)) * target1_Efield_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_1[0].conj() * fft_input_array).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + +def adjoint_propagate_2(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[2*S:3*S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[3*S:4*S]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d)) * target2_Efield_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_2[0].conj() * fft_input_array).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + +def dist_grad_1(): + P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j) + print("shape of P_0_tilde: ", P_0_tilde.shape) + P_0_triangle = P_0 * P_0_tilde + P_0_triangle_nat = np.fft.ifftshift(P_0_triangle) + + P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j) + P_1_triangle = P_1 * P_1_tilde + P_1_triangle_nat = np.fft.ifftshift(P_1_triangle) + + P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j) + P_2_triangle = P_2 * P_2_tilde + P_2_triangle_nat = np.fft.ifftshift(P_2_triangle) + + Phi_1 = np.exp(1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2 = np.exp(1j * phase_mask[S:2*S]).reshape((Nx, Ny)) + Sf = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d)) + S_dagger = np.conj(Sf) + E_t_dagger = np.conj(target1_Efield_2d) + + ################################ + #dC_d0 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d1 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d2 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat + ifft_operator() + + dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + + return np.array([dC_d0, dC_d1, dC_d2]) + +def dist_grad_2(): + P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j) + print("shape of P_0_tilde: ", P_0_tilde.shape) + P_0_triangle = P_0 * P_0_tilde + P_0_triangle_nat = np.fft.ifftshift(P_0_triangle) + + P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j) + P_1_triangle = P_1 * P_1_tilde + P_1_triangle_nat = np.fft.ifftshift(P_1_triangle) + + P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j) + P_2_triangle = P_2 * P_2_tilde + P_2_triangle_nat = np.fft.ifftshift(P_2_triangle) + + Phi_1 = np.exp(1j * phase_mask[2*S:3*S]).reshape((Nx, Ny)) + Phi_2 = np.exp(1j * phase_mask[3*S:4*S]).reshape((Nx, Ny)) + Sf = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d)) + S_dagger = np.conj(Sf) + E_t_dagger = np.conj(target2_Efield_2d) + + ################################ + #dC_d0 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d1 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d2 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat + ifft_operator() + + dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + + return np.array([dC_d0, dC_d1, dC_d2]) + +def dist_norm_to_real(norm_dist): + '''Maps normalized distance parameters to real distance values. + Args: + norm_dist: normalized distance parameters (flattened) + Returns: + Real distance values corresponding to the normalized parameters (flattened)''' + return norm_dist * (distance_upper_bound - distance_lower_bound) + distance_lower_bound + +def _compute_cost_1(): + + forward_propagate_1() + + output_field_1 = intermediate_fields_1[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field_1 * np.conj(target1_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +def _compute_cost_2(): + + forward_propagate_2() + + output_field_2 = intermediate_fields_2[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field_2 * np.conj(target2_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +opt_history_1 = [] +opt_history_2 = [] +opt_cycle = [0] +def cost_fun(x, grad): + ''' Cost function to be minimized. + Args: + x: input normalized parameter field (flattened) + grad: gradient of the cost function with respect to x, modified in place (flattened) + Returns: + The cost function value for the input parameter field x. + ''' + d[:] = dist_norm_to_real(x[:distance_num]) #update distance variables + print("Current distances: ", d) + w_norm[:] = x[distance_num:] #update normalized phase mask parameters + phase_mask[:] = phase_given_w(w_norm) + + compute_phase_factors() #update phase factor matrices + compute_input_field() #update input field based on the new distances + + C1 = _compute_cost_1() + C2 = _compute_cost_2() + C = C1 + C2 #average cost function for both modes + opt_history_1.append(C1) + opt_history_2.append(C2) + + if opt_cycle[0] % 10 == 0: + plt.figure() + plt.plot(opt_history_1, 'r', label="Mode #1", markersize=2) + plt.plot(opt_history_2, 'b', label="Mode #2", markersize=2) + plt.xlabel('Iteration') + plt.ylabel('Cost Function Value') + plt.title('Optimization History') + plt.tight_layout() + plt.legend() + plt.savefig("results/modeconv1_optimization_history.pdf") + plt.close() + + opt_cycle[0] = opt_cycle[0] + 1 + if grad.size > 0: + if opt_cycle[0] < 1000: + grad[:distance_num] = np.zeros(distance_num) #set distance gradient to zero for the first 100 iterations + else: + grad[:distance_num] = dist_grad_1() * (distance_upper_bound - distance_lower_bound) + dist_grad_2() * (distance_upper_bound - distance_lower_bound) + #grad[:distance_num] = np.array([0,0,0]) #set distance gradient to zero for now + grad[distance_num:2*S+distance_num] = adjoint_propagate_1() * 2 * np.pi + grad[2*S+distance_num:4*S+distance_num] = adjoint_propagate_2() * 2 * np.pi + gc.collect() #force garbage collection to free memory + plt.close('all') + return C + +if __name__ == "__main__": + #initialize nlopt solver + solver = nlopt.opt(nlopt.LD_CCSAQ, distance_num + 4*S) + + solver.set_lower_bounds(distance_and_mask_lower_bounds.flatten()) + solver.set_upper_bounds(distance_and_mask_upper_bounds.flatten()) + solver.set_max_objective(cost_fun) + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-7) + solver.set_param("verbosity",1) + + print("Starting optimization...") + start = True + initial_d = np.array([0.4, 0.5, 0.3]) + dist_and_w_norm = np.concatenate((initial_d, w_norm), axis=0) #initialize optimization variables + + if start: + dist_and_w_norm[:] = solver.optimize(dist_and_w_norm) + + print("Optimization completed.") + d[:] = dist_norm_to_real(dist_and_w_norm[:distance_num]) #update distance variables + w_norm[:] = dist_and_w_norm[distance_num:] #update normalized phase mask parameters + print("Optimized distances: ", d) + #verify the results + compute_phase_factors() + compute_input_field() + phase_mask[:] = phase_given_w(w_norm) + forward_propagate_1() + forward_propagate_2() + output_field1 = intermediate_fields_1[-1] + output_field2 = intermediate_fields_2[-1] + + input_power = np.sum(np.abs(input_field)**2) + output_power = np.sum(np.abs(output_field1)**2) + + print("Norm. input power: %.4e" % input_power) + print("Norm. output power: %.4e" % output_power) + + #save the output field for later use + save_output_field = False + if save_output_field: + np.savetxt("results/optimized_output_field.txt", output_field1.reshape((Nx, Ny))) + # Plot results in individual PDF figures + + # Reshape fields to 2D for visualization + output_field1_2d = output_field1.reshape((Nx, Ny)) + output_field2_2d = output_field2.reshape((Nx, Ny)) + phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(w_norm[S:2*S]).reshape((Nx, Ny)) + phase_mask_3 = phase_given_w(w_norm[2*S:3*S]).reshape((Nx, Ny)) + phase_mask_4 = phase_given_w(w_norm[3*S:4*S]).reshape((Nx, Ny)) + + phase_mask_zoom = 150 + make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_3, choose_quantity="phase_mask", save_name="phase_mask_3", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_4, choose_quantity="phase_mask", save_name="phase_mask_4", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + + # Plot: Output field amplitude and phase + make_polarPlot_of(output_field1_2d, choose_quantity="amplitude", save_name="optimized_output1_field_amplitude", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field1_2d, choose_quantity="phase", save_name="optimized_output1_field_phase", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field2_2d, choose_quantity="amplitude", save_name="optimized_output2_field_amplitude", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field2_2d, choose_quantity="phase", save_name="optimized_output2_field_phase", plot_zoom_r=zoom_output_fiber_radius) + + slice_y_index = Ny//2 + slice_x = xs * 1e6 + slice_target = target1_Efield.reshape((Nx, Ny))[slice_y_index,:] + slice_output = output_field1.reshape((Nx, Ny))[slice_y_index,:] + #make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude") + #make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase") + + #save the phase masks for later use + savePhase_masks = False + if savePhase_masks: + np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1) + np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2) + + print("All plots saved to results/ folder.") \ No newline at end of file diff --git a/metaprop_multi_objective/metaprop_fibermode_doublet_distGrad_xy_epigraph.py b/metaprop_multi_objective/metaprop_fibermode_doublet_distGrad_xy_epigraph.py new file mode 100644 index 0000000..1fba6c7 --- /dev/null +++ b/metaprop_multi_objective/metaprop_fibermode_doublet_distGrad_xy_epigraph.py @@ -0,0 +1,954 @@ +''' +This file implements a metasurface doublet for mode conversion. The optimization loop implements two beam paths that are +optimized independetly. This is useful for example when considering distinct polarizations (x and y). The optimization +is based on an epigraph formulation. +''' + +import gc +import time +import multiprocessing + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +import os + +if not os.path.exists("results"): + os.makedirs("results") + +############################## +#physics and simulation domain +wavelength = 1.55e-6 +n0 = 1.0 +n1 = 1.0 +n2 = 1.0 + +lda0 = wavelength / n0 +lda1 = wavelength / n1 +lda2 = wavelength / n2 + +k0 = 2 * np.pi / lda0 +k1 = 2 * np.pi / lda1 +k2 = 2 * np.pi / lda2 + +opt_max_eval = 270 + +unit_cell_pitch = 700e-9 #equivalent to spatial sampling + +Nx = 1024 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d0_init = 300e-6 #propagation distance: source - MS1 +d1_init = 500e-6 #propagation distance: MS1 - MS2 +d2_init = 300e-6 #propagation distance: MS2 - target +distance_lower_bound = 100e-6 +distance_upper_bound = 1000e-6 +distance_num = 3 +d = np.array([d0_init, d1_init, d2_init]) + +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#Source +beam_waist = 5.2e-6 +#output fibre +output_fiber_core_diameter = 19e-6 +############################## + +############################## +#plan pyfftw objects for forward and inverse FFTs +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = np.zeros((Nx, Ny), dtype=complex) +P_0 = np.zeros((Nx, Ny), dtype=complex) +P_0_nat = np.zeros((Nx, Ny), dtype=complex) +phase_factor_1 = np.zeros((Nx, Ny), dtype=complex) +P_1 = np.zeros((Nx, Ny), dtype=complex) +P_1_nat = np.zeros((Nx, Ny), dtype=complex) +phase_factor_2 = np.zeros((Nx, Ny), dtype=complex) +P_2 = np.zeros((Nx, Ny), dtype=complex) +P_2_nat = np.zeros((Nx, Ny), dtype=complex) +P_1_dagger = np.zeros((Nx, Ny), dtype=complex) +P_2_dagger = np.zeros((Nx, Ny), dtype=complex) +P_1_dagger_nat = np.zeros((Nx, Ny), dtype=complex) +P_2_dagger_nat = np.zeros((Nx, Ny), dtype=complex) + +############################## +def compute_phase_factors(): + phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_0[:] = np.exp(1j * phase_factor_0) + P_0_nat[:] = np.fft.ifftshift(P_0) + phase_factor_1[:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_1[:] = np.exp(1j * phase_factor_1) + P_1_nat[:] = np.fft.ifftshift(P_1) + phase_factor_2[:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_2[:] = np.exp(1j * phase_factor_2) + P_2_nat[:] = np.fft.ifftshift(P_2) + + P_1_dagger[:] = P_1.T.conj() + P_2_dagger[:] = P_2.T.conj() + P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj() + P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj() + + +############################## +#Plot zoom parameters [um] +#Plot zoom parameters [um] +zoom_x = 150 +zoom_y = 150 +zoom_output_fiber_radius = 25 +full_view_x = size_x * 1e6 / 2 +full_view_y = size_y * 1e6 / 2 +############################## + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(mode_list:list)->list: + '''Returns the fiber mode pattern for the selected n index. + Args: + mode_list: the list of the selected fiber mode indices + Returns: + The fiber mode patterns corresponding to the selected indices returned + as a list of 2D arrays with the same order as the input mode_list + ''' + #check if the modes in mode_list have already been compute by checking + #the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files. + fiber_modes_input_folder = "fiber_modes" + output_patterns = [] + try: + for enum_index, n in enumerate(mode_list): + #try to load mode index n + try: + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" + pattern = np.load(pattern_file) + output_patterns.append(pattern) + print("Loaded fiber patterns for mode index: ", n) + #if mode index n was not found, compute it + except FileNotFoundError: + if not os.path.exists(fiber_modes_input_folder): + os.makedirs(fiber_modes_input_folder) + + print("Computing fiber patterns for mode index: ", n) + lda = 1.55e-6 + + n_co = 1.4630 #core refractive index + n_cl = 1.4585 #cladding refractive index + + fiber_core_diameter = output_fiber_core_diameter # [m] + a = fiber_core_diameter / 2 #fiber core radius + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + + b_solutions_at_V_obs = [] + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + b_mode_sel = b_solutions_at_V_obs[0] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi) + else:#outside core + E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi) + #save the computed pattern to a file for future use + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy" + np.save(pattern_file, E_field) + output_patterns.append(E_field) + return output_patterns + except: + raise(Exception("Error in computing fiber patterns")) + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the simulation spatial grid by a given physical (true space) amount. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X + shift_amount_x + Yp = Y + shift_amount_y + return Xp, Yp + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates. + Normalization consists in scaling the field such that its integrated intensity is 1. + + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The 2D normalized Gaussian source field. + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + + return gaussian_field_normalized + +def propagate_source(source_to_propagate): + '''Returns the propagated source field (flattened) just before metasurface 1 (MS1). + This involves propagating the source for a distance d0: from its plane of definition up to MS1. + Args: + source_to_propagate: the source field to be propagated (flattened) + Returns: + The propagated source field (flattened) just before MS1. + ''' + + fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten() + + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + plt.plot(x_axis[i], y_axis[i]) + plt.xlabel("x [um]") + plt.tight_layout() + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None: + '''Makes a polar plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_r: zoom level for the radial axis (in micrometers) + Returns: + None + ''' + fig, ax = plt.subplots(figsize=(6, 6)) + if choose_quantity == "amplitude": + given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting + im = ax.imshow(given_field_2d,origin='lower') + elif choose_quantity == "phase": + given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting + im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("argument choose_quantity must be either 'amplitude' or 'phase'") + + center_x, center_y = Nx//2, Ny//2 + radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels + circle = patches.Circle((center_x, center_y), radius, transform=ax.transData) + im.set_clip_path(circle) + ax.set_xlim(center_x - radius, center_x + radius) + ax.set_ylim(center_y - radius, center_y + radius) + ax.axis('off') + cbar = plt.colorbar(im, ax=ax) + plt.savefig("results/" + save_name + ".pdf") + +########################### +#initialize source field +source_field = get_source_field(beam_waist) +source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#source field integrated intensity +source_power = np.sum(np.abs(source_field)**2) +print("Source field integrated intensity: %.4e" % source_power) +#make a plot and save the source field +make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +########################### + +########################### +#initialize input field (propagated source field) +input_field = np.zeros((S,), dtype=complex) +input_field_2d = np.zeros((Nx, Ny), dtype=complex) + +#make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +#make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +def compute_input_field(): + input_field[:] = propagate_source(source_field) + input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting + #input field integrated intensity + input_field_intensity = np.sum(np.abs(input_field)**2) + print("Input field integrated intensity: %.4e" % input_field_intensity) +########################### + +########################### +#initialize target field +target_mode_indices = [1,2] +fiber_mode_pattern1, fiber_mode_pattern2 = get_fiber_mode_pattern(target_mode_indices) +target1_Efield_2d = fiber_mode_pattern1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern1)**2)) +target2_Efield_2d = fiber_mode_pattern2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern2)**2)) +target1_Efield = target1_Efield_2d.flatten() #normalized target field (intensity = 1) +target2_Efield = target2_Efield_2d.flatten() #normalized target field (intensity = 1) + +# Plot: Target field +make_polarPlot_of(target1_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target1_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target2_Efield_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius) +make_polarPlot_of(target2_Efield_2d, choose_quantity="phase", save_name="target_field_2_phase_polar", plot_zoom_r=zoom_output_fiber_radius) +########################### + +########################### +#initialize the normalized phase masks and global phase mask +w_norm = np.zeros((4*S)) #concatenated normalized parameters for both masks +X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny)) +rho = np.sqrt(X**2 + Y**2) +circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0) +#w_norm[:S] += circular_mask_1 +target1_pattern_phase = (np.angle(target1_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi +w_norm[S:2*S] += target1_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 + +target2_pattern_phase = (np.angle(target2_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi +w_norm[3*S:4*S] += target2_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 + +phase_mask = phase_given_w(w_norm) + +#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. +#the bounding box is the outer shell of a square with thickness 100 um +norm_phase_min = 0 +norm_phase_max = 1 +mask_buffer = 100e-6 +mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) +mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) +mask_lower_bounds = np.zeros_like(mask_upper_bounds) +distance_upper_bounds = np.array([1, 1, 1]) +distance_lower_bounds = np.array([0, 0, 0]) +epigraph_upper_bounds = np.array([+np.inf]) +epigraph_lower_bounds = np.array([-np.inf]) +epigraph_distance_and_mask_upper_bounds = np.concatenate((epigraph_upper_bounds, distance_upper_bounds, mask_upper_bounds, mask_upper_bounds),axis=0) +epigraph_distance_and_mask_lower_bounds = np.concatenate((epigraph_lower_bounds, distance_lower_bounds, mask_lower_bounds, mask_lower_bounds),axis=0) +########################### + +intermediate_fields_1 = [None, None] +intermediate_fields_2 = [None, None] +def forward_propagate_1() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields_1[0] = ifft_operator.output_array.copy() + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_1[0])**2)) + + fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:2*S].reshape((Nx, Ny)))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields_1[1] = ifft_operator.output_array.copy() + + end_time = time.time() + make_2Dplot_of(intermediate_fields_1[0], choose_quantity="amplitude", save_name="intermediate_field_1_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(intermediate_fields_1[1], choose_quantity="amplitude", save_name="intermediate_field_1_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + +def forward_propagate_2() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[2*S:3*S])).reshape((Nx, Ny)) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields_2[0] = ifft_operator.output_array.copy() + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_2[0])**2)) + + fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[3*S:4*S].reshape((Nx, Ny)))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields_2[1] = ifft_operator.output_array.copy() + + end_time = time.time() + #make_2Dplot_of(intermediate_fields_2[0], choose_quantity="amplitude", save_name="intermediate_field_2_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(intermediate_fields_2[1], choose_quantity="amplitude", save_name="intermediate_field_2_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + +def adjoint_propagate_1(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:2*S]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d)) * target1_Efield_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_1[0].conj() * fft_input_array).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + +def adjoint_propagate_2(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[2*S:3*S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[3*S:4*S]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d)) * target2_Efield_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_2[0].conj() * fft_input_array).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + +def dist_grad_1(): + P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j) + #print("shape of P_0_tilde: ", P_0_tilde.shape) + P_0_triangle = P_0 * P_0_tilde + P_0_triangle_nat = np.fft.ifftshift(P_0_triangle) + + P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j) + P_1_triangle = P_1 * P_1_tilde + P_1_triangle_nat = np.fft.ifftshift(P_1_triangle) + + P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j) + P_2_triangle = P_2 * P_2_tilde + P_2_triangle_nat = np.fft.ifftshift(P_2_triangle) + + Phi_1 = np.exp(1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2 = np.exp(1j * phase_mask[S:2*S]).reshape((Nx, Ny)) + Sf = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d)) + S_dagger = np.conj(Sf) + E_t_dagger = np.conj(target1_Efield_2d) + + ################################ + #dC_d0 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d1 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d2 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat + ifft_operator() + + dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + + return np.array([dC_d0, dC_d1, dC_d2]) + +def dist_grad_2(): + P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j) + print("shape of P_0_tilde: ", P_0_tilde.shape) + P_0_triangle = P_0 * P_0_tilde + P_0_triangle_nat = np.fft.ifftshift(P_0_triangle) + + P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j) + P_1_triangle = P_1 * P_1_tilde + P_1_triangle_nat = np.fft.ifftshift(P_1_triangle) + + P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j) + P_2_triangle = P_2 * P_2_tilde + P_2_triangle_nat = np.fft.ifftshift(P_2_triangle) + + Phi_1 = np.exp(1j * phase_mask[2*S:3*S]).reshape((Nx, Ny)) + Phi_2 = np.exp(1j * phase_mask[3*S:4*S]).reshape((Nx, Ny)) + Sf = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d)) + S_dagger = np.conj(Sf) + E_t_dagger = np.conj(target2_Efield_2d) + + ################################ + #dC_d0 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d1 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat + ifft_operator() + + dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + ################################ + #dC_d2 + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_1 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat + ifft_operator() + fft_input_array[:,:] = ifft_operator.output_array * Phi_2 + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat + ifft_operator() + + dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array)) + + return np.array([dC_d0, dC_d1, dC_d2]) + +def dist_norm_to_real(norm_dist): + '''Maps normalized distance parameters to real distance values. + Args: + norm_dist: normalized distance parameters (flattened) + Returns: + Real distance values corresponding to the normalized parameters (flattened)''' + return norm_dist * (distance_upper_bound - distance_lower_bound) + distance_lower_bound + +def _compute_cost_1(): + + forward_propagate_1() + + output_field_1 = intermediate_fields_1[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field_1 * np.conj(target1_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +def _compute_cost_2(): + + forward_propagate_2() + + output_field_2 = intermediate_fields_2[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field_2 * np.conj(target2_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +opt_history_1 = [] +opt_history_2 = [] +opt_cycle = [0] +def cost_fun(x): + ''' Cost function to be minimized. + Args: + x: input normalized parameter field (flattened) + grad: gradient of the cost function with respect to x, modified in place (flattened) + Returns: + The cost function value for the input parameter field x. + ''' + d[:] = dist_norm_to_real(x[:distance_num]) #update distance variables + print("Current distances: ", d) + w_norm[:] = x[distance_num:] #update normalized phase mask parameters + phase_mask[:] = phase_given_w(w_norm) + + compute_phase_factors() #update phase factor matrices + compute_input_field() #update input field based on the new distances + + C1 = _compute_cost_1() + C2 = _compute_cost_2() + C = [-C1, -C2] + opt_history_1.append(C1) + opt_history_2.append(C2) + + if opt_cycle[0] % 10 == 0: + plt.figure() + plt.plot(opt_history_1, 'r', label="Mode #1", markersize=2) + plt.plot(opt_history_2, 'b', label="Mode #2", markersize=2) + plt.xlabel('Iteration') + plt.ylabel('Cost Function Value') + plt.title('Optimization History') + plt.tight_layout() + plt.legend() + plt.savefig("results/modeconv1_optimization_history.pdf") + plt.close() + + opt_cycle[0] = opt_cycle[0] + 1 + + grad = np.zeros((2, distance_num + 4*S)) #initialize gradient array + if opt_cycle[0] < opt_max_eval - 20: + grad[:,:distance_num] = np.zeros(distance_num) #set distance gradient to zero for the first 100 iterations + else: + grad[:,:distance_num] = (dist_grad_1() + dist_grad_2()) * (distance_upper_bound - distance_lower_bound) + #grad[:distance_num] = np.array([0,0,0]) #set distance gradient to zero for now + + grad[0, distance_num:] = -np.concatenate((adjoint_propagate_1() * 2 * np.pi, np.zeros(2*S))) + grad[1, distance_num:] = -np.concatenate((np.zeros(2*S), adjoint_propagate_2() * 2 * np.pi)) + + gc.collect() #force garbage collection to free memory + plt.close('all') + + return C, grad + +def epigraph_constraint(result, epigraph_and_weights, gradient) -> None: + epigraph = epigraph_and_weights[0] + d = epigraph_and_weights[1:1+distance_num] + w_norm = epigraph_and_weights[1+distance_num:] + + ob_vals, grad = cost_fun(np.concatenate((d, w_norm))) + if gradient.size > 0: + gradient[:,0] = -1 + gradient[:,1:] = grad + + result[:] = np.real(ob_vals) - epigraph + +epi_opt_history = [] +def epi_cost_fun(epigraph_and_weights, gradient): + epigraph = epigraph_and_weights[0] + + if gradient.size > 0: + gradient[0] = 1 + gradient[1:] = 0 + + epi_opt_history.append(epigraph) + + return epigraph + +if __name__ == "__main__": + #initialize nlopt solver + epigraph_tolerance = np.array([1e-4]*2) + solver = nlopt.opt(nlopt.LD_CCSAQ, 1 + distance_num + 4*S) + print(1 + distance_num + 4*S) + print(np.shape(epigraph_distance_and_mask_lower_bounds.flatten())) + solver.set_lower_bounds(epigraph_distance_and_mask_lower_bounds.flatten()) + solver.set_upper_bounds(epigraph_distance_and_mask_upper_bounds.flatten()) + solver.set_min_objective(epi_cost_fun) + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-5) + solver.set_param("inner_maxeval", 5) + solver.set_param("verbosity",1) + solver.add_inequality_mconstraint( + lambda result_, epigraph_and_weights_, grad_: epigraph_constraint( + result_, + epigraph_and_weights_, + grad_, + ), + epigraph_tolerance, + ) + + print("Starting optimization...") + start = True + initial_d_norm = np.array([0.4, 0.5, 0.3]) + dist_and_w_norm = np.concatenate((initial_d_norm, w_norm), axis=0) #initialize optimization variables + epigraph_dist_and_w_norm = np.insert(dist_and_w_norm, 0, 0, axis=0) #initialize optimization variables with epigraph + if start: + epigraph_dist_and_w_norm[:] = solver.optimize(epigraph_dist_and_w_norm) + + print("Optimization completed.") + final_epigraph = epigraph_dist_and_w_norm[0] + d[:] = dist_norm_to_real(epigraph_dist_and_w_norm[1:1+distance_num]) #update distance variables + w_norm[:] = epigraph_dist_and_w_norm[1+distance_num:] #update normalized phase mask parameters + print("Optimized distances: ", d) + #verify the results + compute_phase_factors() + compute_input_field() + phase_mask[:] = phase_given_w(w_norm) + forward_propagate_1() + forward_propagate_2() + output_field1 = intermediate_fields_1[-1] + output_field2 = intermediate_fields_2[-1] + + input_power = np.sum(np.abs(input_field)**2) + output_power = np.sum(np.abs(output_field1)**2) + + print("Norm. input power: %.4e" % input_power) + print("Norm. output power: %.4e" % output_power) + + #save the output field for later use + save_output_field = False + if save_output_field: + np.savetxt("results/optimized_output_field.txt", output_field1.reshape((Nx, Ny))) + # Plot results in individual PDF figures + + # Reshape fields to 2D for visualization + output_field1_2d = output_field1.reshape((Nx, Ny)) + output_field2_2d = output_field2.reshape((Nx, Ny)) + phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(w_norm[S:2*S]).reshape((Nx, Ny)) + phase_mask_3 = phase_given_w(w_norm[2*S:3*S]).reshape((Nx, Ny)) + phase_mask_4 = phase_given_w(w_norm[3*S:4*S]).reshape((Nx, Ny)) + + phase_mask_zoom = 150 + make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_3, choose_quantity="phase_mask", save_name="phase_mask_3", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + make_2Dplot_of(phase_mask_4, choose_quantity="phase_mask", save_name="phase_mask_4", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + + # Plot: Output field amplitude and phase + make_polarPlot_of(output_field1_2d, choose_quantity="amplitude", save_name="optimized_output1_field_amplitude", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field1_2d, choose_quantity="phase", save_name="optimized_output1_field_phase", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field2_2d, choose_quantity="amplitude", save_name="optimized_output2_field_amplitude", plot_zoom_r=zoom_output_fiber_radius) + make_polarPlot_of(output_field2_2d, choose_quantity="phase", save_name="optimized_output2_field_phase", plot_zoom_r=zoom_output_fiber_radius) + + #compute final overlap value + overlap1 = np.abs(np.sum(output_field1.flatten() * np.conj(target1_Efield))) ** 2 + overlap2 = np.abs(np.sum(output_field2.flatten() * np.conj(target2_Efield))) ** 2 + print("Final overlap with target mode 1: %.4e" % overlap1) + print("Final overlap with target mode 2: %.4e" % overlap2) + + slice_y_index = Ny//2 + slice_x = xs * 1e6 + slice_target = target1_Efield.reshape((Nx, Ny))[slice_y_index,:] + slice_output = output_field1.reshape((Nx, Ny))[slice_y_index,:] + #make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude") + #make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase") + + #save the phase masks for later use + savePhase_masks = False + if savePhase_masks: + np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1) + np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2) + + print("All plots saved to results/ folder.") \ No newline at end of file diff --git a/metaprop_multi_objective/metaprop_fibermode_doublet_epigraph.py b/metaprop_multi_objective/metaprop_fibermode_doublet_epigraph.py new file mode 100644 index 0000000..06bc55a --- /dev/null +++ b/metaprop_multi_objective/metaprop_fibermode_doublet_epigraph.py @@ -0,0 +1,659 @@ +import time +import multiprocessing + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +############################## +#physics and simulation domain +wavelength = 1.55e-6 +k0 = 2 * np.pi / wavelength + +opt_max_eval = 250 + +unit_cell_pitch = 400e-9 #equivalent to spatial sampling + +Nx = 2048 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d0 = 345e-6 #propagation distance: source - MS1 +d1 = 500e-6 #propagation distance: MS1 - MS2 +d2 = 300e-6 #propagation distance: MS2 - target +d = [d1, d2] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target + +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#optimization algorithm +epigraph_tolerance = np.array([1e-4]*2) +#epigraph_tolerance = np.array([1e-4]) +############################## + +############################## +#Source +beam_waist = 9e-6 +source_spacing = 300e-6 +############################## + +############################## +#plan pyfftw objects for forward and inverse FFTs +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = k0 * d0 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_0 = np.exp(1j * phase_factor_0) +P_0_nat = np.fft.ifftshift(P_0) +phase_factor_1 = k0 * d1 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_1 = np.exp(1j * phase_factor_1) +P_1_nat = np.fft.ifftshift(P_1) +phase_factor_2 = k0 * d2 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_2 = np.exp(1j * phase_factor_2) +P_2_nat = np.fft.ifftshift(P_2) + +P_1_dagger = P_1.T.conj() +P_2_dagger = P_2.T.conj() +P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj() +P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj() +############################## + +############################## +#Plot zoom parameters [um] +zoom_x = 60 +zoom_y = 60 +zoom_core_diameter = 2 * 6 +full_view_x = size_x * 1e6 / 2 +full_view_y = size_y * 1e6 / 2 +############################## + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(mode_list:list)->list: + '''Returns the fiber mode pattern for the selected n index. + Args: + mode_list: the list of the selected fiber mode indices + Returns: + The fiber mode patterns corresponding to the selected indices returned + as a list of 2D arrays with the same order as the input mode_list + ''' + #check if the modes in mode_list have already been compute by checking + #the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files. + + print("Computing fiber patterns for mode indices: ", mode_list) + lda = 1.55e-6 + + n_co = 1.4630 #core refractive index + n_cl = 1.4585 #cladding refractive index + a = 6e-6 #fiber core radius + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + + b_solutions_at_V_obs = [] + for n in mode_list: + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + output_patterns = [] + for enum_index,n_mode_sel in enumerate(mode_list): + b_mode_sel = b_solutions_at_V_obs[enum_index] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n_mode_sel,chi_co * r) / jv(n_mode_sel, chi_co * a) * np.cos(n_mode_sel * phi) + else:#outside core + E_field[i,j] = kv(n_mode_sel, chi_cl * r) / kv(n_mode_sel, chi_cl * a) * np.cos(n_mode_sel * phi) + + output_patterns.append(E_field) + return output_patterns + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a normalized Gaussian source field for a given beam waist. Normalization consists in scaling + the field such that its integrated intensity is 1. + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The normalized Gaussian source field + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + return gaussian_field_normalized + +def propagate_source(source_to_propagate): + '''Returns the propagated source field (flattened) just before metasurface 1 (MS1). This involves propagating the + source for a distance d0: from its plane of definition up to MS1.''' + fft_input_array[:,:] = source_to_propagate.reshape((Nx, Ny)) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = ifft_operator.output_array.flatten() + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,legend_labels,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + if len(legend_labels) != len(x_axis): + raise ValueError("Length of legend_labels must match the number of plots.") + else: + plt.plot(x_axis[i], y_axis[i], label=legend_labels[i]) + + plt.legend() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None: + '''Makes a polar plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_r: zoom level for the radial axis (in micrometers) + Returns: + None + ''' + _, ax = plt.subplots(figsize=(6, 6)) + if choose_quantity == "amplitude": + given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting + im = ax.imshow(given_field_2d,origin='lower') + elif choose_quantity == "phase": + given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting + im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + center_x, center_y = Nx//2, Ny//2 + radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels + circle = patches.Circle((center_x, center_y), radius, transform=ax.transData) + im.set_clip_path(circle) + ax.set_xlim(center_x - radius, center_x + radius) + ax.set_ylim(center_y - radius, center_y + radius) + ax.axis('off') + cbar = plt.colorbar(im, ax=ax) + plt.savefig("results/" + save_name + ".pdf") + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the spatial grid by a given amount. This is useful for centering the source field on the metasurface. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X + shift_amount_x + Yp = Y + shift_amount_y + return Xp, Yp + +def shift_grid(array_to_shift,shift_amount_x,shift_amount_y): + '''Shifts a given 2D array by a given amount. + Args: + array_to_shift: the 2D array to be shifted + shift_amount_x: amount to shift the array in x direction (in meters) + shift_amount_y: amount to shift the array in y direction (in meters) + Returns: + the shifted 2D array''' + pass + + +########################### +#initialize source field +source_field_1 = get_source_field(beam_waist,center_x=-source_spacing/2,center_y=0) +source_field_1_2d = source_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting + +source_field_2 = get_source_field(beam_waist,center_x=source_spacing/2,center_y=0) +source_field_2_2d = source_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting + +#source field integrated intensity +source_1_power = np.sum(np.abs(source_field_1)**2) +source_2_power = np.sum(np.abs(source_field_2)**2) +print("Source 1 field integrated intensity: %.4e" % source_1_power) +print("Source 2 field integrated intensity: %.4e" % source_2_power) +#make a plot and save the source field +make_2Dplot_of(source_field_1_2d, choose_quantity="amplitude", save_name="source_1_field_amplitude", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y) +make_2Dplot_of(source_field_1_2d, choose_quantity="phase", save_name="source_1_field_phase", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y) +make_2Dplot_of(source_field_2_2d, choose_quantity="amplitude", save_name="source_2_field_amplitude", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y) +make_2Dplot_of(source_field_2_2d, choose_quantity="phase", save_name="source_2_field_phase", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y) +########################### + +########################### +#initialize input field 1 (propagated source 1 field) +input_field_1 = propagate_source(source_field_1) +input_field_1_2d = input_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting + +#initialize input field 2 (propagated source 2 field) +input_field_2 = propagate_source(source_field_2) +input_field_2_2d = input_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting + +#input field 1 integrated intensity +input_field_1_intensity = np.sum(np.abs(input_field_1)**2) +print("Input field integrated intensity: %.4e" % input_field_1_intensity) + +#input field 2 integrated intensity +input_field_2_intensity = np.sum(np.abs(input_field_2)**2) +print("Input field integrated intensity: %.4e" % input_field_2_intensity) + +#make a plot and save the input field +make_2Dplot_of(input_field_1_2d, choose_quantity="amplitude", save_name="input_field_1_amplitude", plot_zoom_x=zoom_x*3, plot_zoom_y=zoom_y*3) +make_2Dplot_of(input_field_1_2d, choose_quantity="phase", save_name="input_field_1_phase", plot_zoom_x=zoom_x*3, plot_zoom_y=zoom_y*3) +make_2Dplot_of(input_field_2_2d, choose_quantity="amplitude", save_name="input_field_2_amplitude", plot_zoom_x=zoom_x*3, plot_zoom_y=zoom_y*3) +make_2Dplot_of(input_field_2_2d, choose_quantity="phase", save_name="input_field_2_phase", plot_zoom_x=zoom_x*3, plot_zoom_y=zoom_y*3) + +#list of input fields to be propagated within the optimization loop +input_field_list = [input_field_1, input_field_2] +#input_field_list = [input_field_1] +########################### + +########################### +#initialize target field +target_mode_indices = [1,2] +fiber_mode_pattern_1, fiber_mode_pattern_2 = get_fiber_mode_pattern(target_mode_indices) + +target_Efield_1_2d = fiber_mode_pattern_1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_1)**2)) +target_Efield_1 = target_Efield_1_2d.flatten() #normalized target field (intensity = 1) + +target_Efield_2_2d = fiber_mode_pattern_2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_2)**2)) +target_Efield_2 = target_Efield_2_2d.flatten() #normalized target field (intensity = 1) + +# Plot: Target fields +make_2Dplot_of(target_Efield_1_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(target_Efield_1_2d, choose_quantity="phase", save_name="target_field_1_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(target_Efield_2_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(target_Efield_2_2d, choose_quantity="phase", save_name="target_field_2_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) + +target_field_list = [target_Efield_1, target_Efield_2] +#target_field_list = [target_Efield_1] +########################### + +def forward_propagate(input_field:np.ndarray,weights:np.ndarray) -> np.ndarray: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + the output field computed at the observation plane + ''' + print("Starting propagating...") + intermediate_fields = [None, None] + start_time = time.time() + phase_mask = phase_given_w(weights) + fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + #FIRST PROPAGATION + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy()) + #print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2)) + + fft_input_array[:,:] = np.fft.ifftshift((np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny))))) + fft_operator() + + #SECOND PROPAGATION + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy()) + + end_time = time.time() + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + return intermediate_fields + +def adjoint_propagate(input_field_2d,target_Efield_2d,intermediate_fields): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger) + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + + +def value_and_grad(weights): + values = [] + gradients = [] + + phase_mask[:] = phase_given_w(weights) #update phase mask in place + for i, input_field in enumerate(input_field_list): + + intermediate_fields = forward_propagate(input_field, weights) + + circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(intermediate_fields[-1].flatten() * circular_mask * np.conj(target_field_list[i]))) ** 2 + C = np.real(np.sum(C_s)) + + values.append(-C) + gradients.append( + adjoint_propagate( + input_field_2d = input_field.reshape((Nx, Ny)), + target_Efield_2d = target_field_list[i].reshape((Nx, Ny)), + intermediate_fields = intermediate_fields) + ) + + return np.array(values), np.array(gradients) + +obj_values1 = [] +obj_values2 = [] +def epigraph_constraint(result,epigraph_and_weights,gradient)->None: + '''Constraint function for the epigraph formulation + Args: + result: 1D array, modified in place, with the results of this constraint + evaluation + epigraph_and_weights: 1D array of epigraph (first element) and + design weights (remaining elements) + gradient: gradients, modified in place, of the constraint function with respect to the design weights + ''' + epigraph = epigraph_and_weights[0] + weights = epigraph_and_weights[1:] + + obj_val, grad = value_and_grad(weights) + obj_values1.append(obj_val[0]) + obj_values2.append(obj_val[1]) + + if gradient.size > 0: + gradient[:,0] = -1 + gradient[:,1:] = - grad * (2*np.pi) + + #result of the evaluation of this constraint function + result[:] = np.real(obj_val) - epigraph + +opt_history = [] +def obj_fun(epigraph_and_weights, grad): + '''Objective function for the epigraph formulation + Args: + epigraph_and_weights: 1D array of epigraph (first element) and + design weights (remaining elements) + grad: gradient of the objective function with respect to the design weights, modified in place + Returns: + The scalar epigraph variable (the objective function) + ''' + + epigraph = epigraph_and_weights[0] + + if grad.size > 0: + grad[0] = 1 + grad[1:] = 0 + + opt_history.append(epigraph) + #make a 1d plot of the optimization history together with the objective values + make_1Dplot_of(x_axis=[np.arange(len(obj_values1)), np.arange(len(obj_values2)), np.arange(len(opt_history))], y_axis=[obj_values1, obj_values2, opt_history], legend_labels=["ob1","ob2","t"], save_name="optimization_history", plot_zoom_x=len(opt_history)) + + return epigraph + +def initialize_masks(): + X1,Y1 = shift_spatial_grid(-source_spacing/2,0) + rho1 = np.sqrt(X1**2 + Y1**2) + circular_mask_1 = np.where(rho1.flatten() < 10*6e-6, 1, 0) + + X2,Y2 = shift_spatial_grid(source_spacing/2,0) + rho2 = np.sqrt(X2**2 + Y2**2) + circular_mask_2 = np.where(rho2.flatten() < 10*6e-6, 1, 0) + + target_pattern_1_norm_phase = (np.angle(target_Efield_1) + 2 * np.pi) % (2 * np.pi) / (2*np.pi) + target_pattern_1_norm_phase = np.roll(target_pattern_1_norm_phase, shift=int(source_spacing/sampling_period/2), axis=0) + + target_pattern_2_norm_phase = (np.angle(target_Efield_2) + 2 * np.pi) % (2 * np.pi) / (2*np.pi) + target_pattern_2_norm_phase = np.roll(target_pattern_2_norm_phase, shift=-int(source_spacing/sampling_period/2), axis=0) + #on metasurface 1, set initial guess on normalized phase elements + w_norm[:S] += circular_mask_1 * target_pattern_1_norm_phase + circular_mask_2 * target_pattern_2_norm_phase + #on metasurface 2, generate random initial guess for normalized phase elements + #w_norm[S:] += np.random.rand(S) * circular_mask_1 + np.random.rand(S) * circular_mask_2 + +if __name__ == "__main__": + ########################### + #initialize the normalized phase masks and global phase mask + w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks + initialize_masks() + phase_mask = phase_given_w(w_norm) + + #make an initial plot of the phase masks + make_2Dplot_of(phase_mask[:S].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(phase_mask[S:].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + #define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. + #the bounding box is the outer shell of a square with thickness 100 um + norm_phase_min = 0.0 + norm_phase_max = 1.0 + mask_buffer = 100e-6 + mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) + mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) + mask_lower_bounds = np.zeros_like(mask_upper_bounds) + + + epigraph_and_weights = np.insert(w_norm,0,0) #initialize epigraph variable to 0 + print("#####Shape of epigraph_and_weights: ", epigraph_and_weights.shape) + ########################### + + #initialize nlopt solver + solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S + 1) #weights (2S) + epigraph (1) + epigraph_and_weights_lower_bounds = np.insert(mask_lower_bounds.flatten(),0,-np.inf) + epigraph_and_weights_upper_bounds = np.insert(mask_upper_bounds.flatten(),0,+np.inf) + solver.set_lower_bounds(epigraph_and_weights_lower_bounds) + solver.set_upper_bounds(epigraph_and_weights_upper_bounds) + solver.set_min_objective(obj_fun) + solver.add_inequality_mconstraint( + lambda result_, epigraph_and_weights_, grad_: epigraph_constraint( + result_, + epigraph_and_weights_, + grad_, + ), + epigraph_tolerance, + ) + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-5) + solver.set_param("verbosity", 1) + solver.set_param("inner_maxeval", 10) + + print("Starting optimization...") + start = True + if start: + epigraph_and_weights[:] = solver.optimize(epigraph_and_weights) + print("Optimization completed.") + + #verify the results + opt_weights = epigraph_and_weights[1:] + verify_out_field_1 = forward_propagate(input_field_list[0], opt_weights)[-1] + verify_out_field_2 = forward_propagate(input_field_list[1], opt_weights)[-1] + all_source_on_field = input_field_list[0] + input_field_list[1] + verify_out_field_all = forward_propagate(all_source_on_field, opt_weights)[-1] + + make_polarPlot_of(verify_out_field_1, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source1_amplitude", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_1, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source1_phase", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_2, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source2_amplitude", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_2, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source2_phase", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_all, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_allSource_amplitude", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_all, choose_quantity="phase", save_name="modeconv1_optimized_output_field_allSource_phase", plot_zoom_r=zoom_core_diameter) + + #save the output fields for later use + saveFields = False + if saveFields: + np.savetxt("results/modeconv1_optimized_output_field_source1.txt", verify_out_field_1.reshape((Nx, Ny))) + np.savetxt("results/modeconv1_optimized_output_field_source2.txt", verify_out_field_2.reshape((Nx, Ny))) + np.savetxt("results/modeconv1_optimized_output_field_allSource.txt", verify_out_field_all.reshape((Nx, Ny))) + + # Reshape fields to 2D for visualization + phase_mask_1 = phase_given_w(epigraph_and_weights[1:S+1]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(epigraph_and_weights[S+1:]).reshape((Nx, Ny)) + # Plot: Phase masks + make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + #save the phase masks for later use + saveMasks = False + if saveMasks: + np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1) + np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2) + + print("All plots saved to results/ folder.") \ No newline at end of file diff --git a/metaprop_multi_objective/metaprop_fibermode_doublet_epigraph_ego.py b/metaprop_multi_objective/metaprop_fibermode_doublet_epigraph_ego.py new file mode 100644 index 0000000..38170fb --- /dev/null +++ b/metaprop_multi_objective/metaprop_fibermode_doublet_epigraph_ego.py @@ -0,0 +1,780 @@ +import time +import multiprocessing +import os + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +from smt.applications.ego import EGO, KRG +from smt.sampling_methods import LHS +from smt.design_space import ( + DesignSpace, +) + +############################## +#physics and simulation domain +wavelength = 1.55e-6 +k0 = 2 * np.pi / wavelength + +opt_max_eval = 150 + +unit_cell_pitch = 400e-9 #equivalent to spatial sampling + +Nx = 2048 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d = [0, 0, 0] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target +d0 = d[0] #propagation distance: source - MS1 +d1 = d[1] #propagation distance: MS1 - MS2 +d2 = d[2] #propagation distance: MS2 - target +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#optimization algorithm +epigraph_tolerance = np.array([1e-4]*2) +#epigraph_tolerance = np.array([1e-4]) +############################## + +############################## +#Source +beam_waist = 9e-6 +source_spacing = 300e-6 +############################## + +############################## +#plan pyfftw objects for forward and inverse FFTs +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#Plot zoom parameters [um] +zoom_x = 60 +zoom_y = 60 +zoom_core_diameter = 2 * 6 +full_view_x = size_x * 1e6 / 2 +full_view_y = size_y * 1e6 / 2 +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_0 = np.exp(1j * phase_factor_0) +P_0_nat = np.fft.ifftshift(P_0) +phase_factor_1 = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_1 = np.exp(1j * phase_factor_1) +P_1_nat = np.fft.ifftshift(P_1) +phase_factor_2 = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_2 = np.exp(1j * phase_factor_2) +P_2_nat = np.fft.ifftshift(P_2) + +P_1_dagger = P_1.T.conj() +P_2_dagger = P_2.T.conj() +P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj() +P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj() +############################## + +def update_phase_factors(): + print("Updating phase factors using d=",d) + phase_factor_0[:,:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_0[:,:] = np.exp(1j * phase_factor_0) + P_0_nat[:,:] = np.fft.ifftshift(P_0) + phase_factor_1[:,:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_1[:,:] = np.exp(1j * phase_factor_1) + P_1_nat[:,:] = np.fft.ifftshift(P_1) + phase_factor_2[:,:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_2[:,:] = np.exp(1j * phase_factor_2) + P_2_nat[:,:] = np.fft.ifftshift(P_2) + + P_1_dagger[:] = P_1.T.conj() + P_2_dagger[:] = P_2.T.conj() + P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj() + P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj() + +def update_input_fields(): + input_field_1[:] = propagate_source(source_field_1) + input_field_1_2d[:] = input_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting + + #initialize input field 2 (propagated source 2 field) + input_field_2[:] = propagate_source(source_field_2) + input_field_2_2d[:] = input_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting + + #input field 1 integrated intensity + input_field_1_intensity = np.sum(np.abs(input_field_1)**2) + print("Input field integrated intensity: %.4e" % input_field_1_intensity) + + #input field 2 integrated intensity + input_field_2_intensity = np.sum(np.abs(input_field_2)**2) + print("Input field integrated intensity: %.4e" % input_field_2_intensity) + + #make a plot and save the input field + #make_2Dplot_of(input_field_1_2d, choose_quantity="amplitude", save_name="input_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(input_field_1_2d, choose_quantity="phase", save_name="input_field_1_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(input_field_2_2d, choose_quantity="amplitude", save_name="input_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(input_field_2_2d, choose_quantity="phase", save_name="input_field_2_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(mode_list:list)->list: + '''Returns the fiber mode pattern for the selected n index. + Args: + mode_list: the list of the selected fiber mode indices + Returns: + The fiber mode patterns corresponding to the selected indices returned + as a list of 2D arrays with the same order as the input mode_list + ''' + #check if the modes in mode_list have already been compute by checking + #the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files. + fiber_modes_input_folder = "fiber_modes" + output_patterns = [] + try: + for enum_index, n in enumerate(mode_list): + #try to load mode index n + try: + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy" + pattern = np.load(pattern_file) + output_patterns.append(pattern) + print("Loaded fiber patterns for mode index: ", n) + #if mode index n was not found, compute it + except FileNotFoundError: + if not os.path.exists(fiber_modes_input_folder): + os.makedirs(fiber_modes_input_folder) + + print("Computing fiber patterns for mode index: ", n) + lda = 1.55e-6 + + n_co = 1.4630 #core refractive index + n_cl = 1.4585 #cladding refractive index + + fiber_core_diameter = 12e-6 # [m] + a = fiber_core_diameter / 2 #fiber core radius + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + + b_solutions_at_V_obs = [] + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + b_mode_sel = b_solutions_at_V_obs[0] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi) + else:#outside core + E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi) + #save the computed pattern to a file for future use + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy" + np.save(pattern_file, E_field) + output_patterns.append(E_field) + return output_patterns + except: + raise(Exception("Error in computing fiber patterns")) + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a normalized Gaussian source field for a given beam waist. Normalization consists in scaling + the field such that its integrated intensity is 1. + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The normalized Gaussian source field + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + return gaussian_field_normalized + +def propagate_source(source_to_propagate): + '''Returns the propagated source field (flattened) just before metasurface 1 (MS1). This involves propagating the + source for a distance d0: from its plane of definition up to MS1.''' + fft_input_array[:,:] = source_to_propagate.reshape((Nx, Ny)) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = ifft_operator.output_array.flatten() + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,legend_labels,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + if len(legend_labels) != len(x_axis): + raise ValueError("Length of legend_labels must match the number of plots.") + else: + plt.plot(x_axis[i], y_axis[i], label=legend_labels[i]) + + plt.legend() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None: + '''Makes a polar plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_r: zoom level for the radial axis (in micrometers) + Returns: + None + ''' + _, ax = plt.subplots(figsize=(6, 6)) + if choose_quantity == "amplitude": + given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting + im = ax.imshow(given_field_2d,origin='lower') + elif choose_quantity == "phase": + given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting + im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + center_x, center_y = Nx//2, Ny//2 + radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels + circle = patches.Circle((center_x, center_y), radius, transform=ax.transData) + im.set_clip_path(circle) + ax.set_xlim(center_x - radius, center_x + radius) + ax.set_ylim(center_y - radius, center_y + radius) + ax.axis('off') + cbar = plt.colorbar(im, ax=ax) + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the spatial grid by a given amount. This is useful for centering the source field on the metasurface. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X + shift_amount_x + Yp = Y + shift_amount_y + return Xp, Yp + +########################### +#initialize source field +source_field_1 = get_source_field(beam_waist,center_x=-source_spacing/2,center_y=0) +source_field_1_2d = source_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting + +source_field_2 = get_source_field(beam_waist,center_x=source_spacing/2,center_y=0) +source_field_2_2d = source_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting + +#source field integrated intensity +source_1_power = np.sum(np.abs(source_field_1)**2) +source_2_power = np.sum(np.abs(source_field_2)**2) +print("Source 1 field integrated intensity: %.4e" % source_1_power) +print("Source 2 field integrated intensity: %.4e" % source_2_power) +#make a plot and save the source field +#make_2Dplot_of(source_field_1_2d, choose_quantity="amplitude", save_name="source_1_field_amplitude", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y) +#make_2Dplot_of(source_field_1_2d, choose_quantity="phase", save_name="source_1_field_phase", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y) +#make_2Dplot_of(source_field_2_2d, choose_quantity="amplitude", save_name="source_2_field_amplitude", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y) +#make_2Dplot_of(source_field_2_2d, choose_quantity="phase", save_name="source_2_field_phase", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y) +########################### + +########################### +#initialize input field 1 (propagated source 1 field) +input_field_1 = propagate_source(source_field_1) +input_field_1_2d = input_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting + +#initialize input field 2 (propagated source 2 field) +input_field_2 = propagate_source(source_field_2) +input_field_2_2d = input_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting + +#input field 1 integrated intensity +input_field_1_intensity = np.sum(np.abs(input_field_1)**2) +print("Input field integrated intensity: %.4e" % input_field_1_intensity) + +#input field 2 integrated intensity +input_field_2_intensity = np.sum(np.abs(input_field_2)**2) +print("Input field integrated intensity: %.4e" % input_field_2_intensity) + +#list of input fields to be propagated within the optimization loop +input_field_list = [input_field_1, input_field_2] +#input_field_list = [input_field_1] +########################### + +########################### +#initialize target field +target_mode_indices = [1,2] +fiber_mode_pattern_1, fiber_mode_pattern_2 = get_fiber_mode_pattern(target_mode_indices) + +target_Efield_1_2d = fiber_mode_pattern_1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_1)**2)) +target_Efield_1 = target_Efield_1_2d.flatten() #normalized target field (intensity = 1) + +target_Efield_2_2d = fiber_mode_pattern_2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_2)**2)) +target_Efield_2 = target_Efield_2_2d.flatten() #normalized target field (intensity = 1) + +# Plot: Target fields +#make_2Dplot_of(target_Efield_1_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +#make_2Dplot_of(target_Efield_1_2d, choose_quantity="phase", save_name="target_field_1_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +#make_2Dplot_of(target_Efield_2_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +#make_2Dplot_of(target_Efield_2_2d, choose_quantity="phase", save_name="target_field_2_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) + +target_field_list = [target_Efield_1, target_Efield_2] +#target_field_list = [target_Efield_1] +########################### + +def forward_propagate(input_field:np.ndarray,weights:np.ndarray) -> np.ndarray: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + the output field computed at the observation plane + ''' + print("Starting propagating...") + intermediate_fields = [None, None] + start_time = time.time() + phase_mask = phase_given_w(weights) + fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + #FIRST PROPAGATION + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy()) + #print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2)) + + fft_input_array[:,:] = np.fft.ifftshift((np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny))))) + fft_operator() + + #SECOND PROPAGATION + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy()) + + end_time = time.time() + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + #make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + return intermediate_fields + +def adjoint_propagate(input_field_2d,target_Efield_2d,intermediate_fields): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger) + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + + +def value_and_grad(weights): + values = [] + gradients = [] + + phase_mask[:] = phase_given_w(weights) #update phase mask in place + for i, input_field in enumerate(input_field_list): + + intermediate_fields = forward_propagate(input_field, weights) + + circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(intermediate_fields[-1].flatten() * circular_mask * np.conj(target_field_list[i]))) ** 2 + C = np.real(np.sum(C_s)) + + values.append(-C) + gradients.append( + adjoint_propagate( + input_field_2d = input_field.reshape((Nx, Ny)), + target_Efield_2d = target_field_list[i].reshape((Nx, Ny)), + intermediate_fields = intermediate_fields) + ) + + return np.array(values), np.array(gradients) + +obj_values1 = [] +obj_values2 = [] +def epigraph_constraint(result,epigraph_and_weights,gradient)->None: + '''Constraint function for the epigraph formulation + Args: + result: 1D array, modified in place, with the results of this constraint + evaluation + epigraph_and_weights: 1D array of epigraph (first element) and + design weights (remaining elements) + gradient: gradients, modified in place, of the constraint function with respect to the design weights + ''' + epigraph = epigraph_and_weights[0] + weights = epigraph_and_weights[1:] + + obj_val, grad = value_and_grad(weights) + obj_values1.append(obj_val[0]) + obj_values2.append(obj_val[1]) + + if gradient.size > 0: + gradient[:,0] = -1 + gradient[:,1:] = - grad * (2*np.pi) + + #result of the evaluation of this constraint function + result[:] = np.real(obj_val) - epigraph + +opt_history = [] +def obj_fun(epigraph_and_weights, grad): + '''Objective function for the epigraph formulation + Args: + epigraph_and_weights: 1D array of epigraph (first element) and + design weights (remaining elements) + grad: gradient of the objective function with respect to the design weights, modified in place + Returns: + The scalar epigraph variable (the objective function) + ''' + + epigraph = epigraph_and_weights[0] + + if grad.size > 0: + grad[0] = 1 + grad[1:] = 0 + + opt_history.append(epigraph) + #make a 1d plot of the optimization history together with the objective values + make_1Dplot_of(x_axis=[np.arange(len(obj_values1)), np.arange(len(obj_values2)), np.arange(len(opt_history))], y_axis=[obj_values1, obj_values2, opt_history], legend_labels=["ob1","ob2","t"], save_name="optimization_history", plot_zoom_x=len(opt_history)) + + return epigraph + +def initialize_masks(): + X1,Y1 = shift_spatial_grid(-source_spacing/2,0) + rho1 = np.sqrt(X1**2 + Y1**2) + circular_mask_1 = np.where(rho1.flatten() < 10*6e-6, 1, 0) + + X2,Y2 = shift_spatial_grid(source_spacing/2,0) + rho2 = np.sqrt(X2**2 + Y2**2) + circular_mask_2 = np.where(rho2.flatten() < 10*6e-6, 1, 0) + + target_pattern_1_norm_phase = (np.angle(target_Efield_1) + 2 * np.pi) % (2 * np.pi) / (2*np.pi) + target_pattern_1_norm_phase = np.roll(target_pattern_1_norm_phase, shift=int(source_spacing/sampling_period/2), axis=0) + + target_pattern_2_norm_phase = (np.angle(target_Efield_2) + 2 * np.pi) % (2 * np.pi) / (2*np.pi) + target_pattern_2_norm_phase = np.roll(target_pattern_2_norm_phase, shift=-int(source_spacing/sampling_period/2), axis=0) + #on metasurface 1, set initial guess on normalized phase elements + w_norm[:S] += circular_mask_1 * target_pattern_1_norm_phase + circular_mask_2 * target_pattern_2_norm_phase + #on metasurface 2, generate random initial guess for normalized phase elements + #w_norm[S:] += np.random.rand(S) * circular_mask_1 + np.random.rand(S) * circular_mask_2 + +w_norm = np.zeros((2*S)) #global definition concatenated normalized parameters for both masks +phase_mask = phase_given_w(w_norm) #global definition +epigraph_and_weights = np.zeros((2*S + 1)) + +def metaprop_solve(): + ########################### + #initialize the normalized phase masks and global phase mask + initialize_masks() + phase_mask[:] = phase_given_w(w_norm) + + #make an initial plot of the phase masks + #make_2Dplot_of(phase_mask[:S].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(phase_mask[S:].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + #define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. + #the bounding box is the outer shell of a square with thickness 100 um + norm_phase_min = 0.0 + norm_phase_max = 1.0 + mask_buffer = 100e-6 + mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) + mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) + mask_lower_bounds = np.zeros_like(mask_upper_bounds) + + epigraph_and_weights[:] = np.insert(w_norm,0,0) #initialize epigraph variable to 0 + print("#####Shape of epigraph_and_weights: ", epigraph_and_weights.shape) + ########################### + + #initialize nlopt solver + solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S + 1) #weights (2S) + epigraph (1) + epigraph_and_weights_lower_bounds = np.insert(mask_lower_bounds.flatten(),0,-np.inf) + epigraph_and_weights_upper_bounds = np.insert(mask_upper_bounds.flatten(),0,+np.inf) + solver.set_lower_bounds(epigraph_and_weights_lower_bounds) + solver.set_upper_bounds(epigraph_and_weights_upper_bounds) + solver.set_min_objective(obj_fun) + solver.add_inequality_mconstraint( + lambda result_, epigraph_and_weights_, grad_: epigraph_constraint( + result_, + epigraph_and_weights_, + grad_, + ), + epigraph_tolerance, + ) + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-5) + solver.set_param("verbosity", 1) + solver.set_param("inner_maxeval", 5) + + print("Starting optimization...") + start = True + if start: + epigraph_and_weights[:] = solver.optimize(epigraph_and_weights) + print("Optimization completed.") + + #verify the results + opt_weights = epigraph_and_weights[1:] + verify_out_field_1 = forward_propagate(input_field_list[0], opt_weights)[-1] + verify_out_field_2 = forward_propagate(input_field_list[1], opt_weights)[-1] + all_source_on_field = input_field_list[0] + input_field_list[1] + verify_out_field_all = forward_propagate(all_source_on_field, opt_weights)[-1] + + #make_polarPlot_of(verify_out_field_1, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source1_amplitude", plot_zoom_r=zoom_core_diameter) + #make_polarPlot_of(verify_out_field_1, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source1_phase", plot_zoom_r=zoom_core_diameter) + #make_polarPlot_of(verify_out_field_2, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source2_amplitude", plot_zoom_r=zoom_core_diameter) + #make_polarPlot_of(verify_out_field_2, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source2_phase", plot_zoom_r=zoom_core_diameter) + #make_polarPlot_of(verify_out_field_all, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_allSource_amplitude", plot_zoom_r=zoom_core_diameter) + #make_polarPlot_of(verify_out_field_all, choose_quantity="phase", save_name="modeconv1_optimized_output_field_allSource_phase", plot_zoom_r=zoom_core_diameter) + + #save the output fields for later use + saveFields = False + if saveFields: + np.savetxt("results/modeconv1_optimized_output_field_source1.txt", verify_out_field_1.reshape((Nx, Ny))) + np.savetxt("results/modeconv1_optimized_output_field_source2.txt", verify_out_field_2.reshape((Nx, Ny))) + np.savetxt("results/modeconv1_optimized_output_field_allSource.txt", verify_out_field_all.reshape((Nx, Ny))) + + # Reshape fields to 2D for visualization + phase_mask_1 = phase_given_w(epigraph_and_weights[1:S+1]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(epigraph_and_weights[S+1:]).reshape((Nx, Ny)) + # Plot: Phase masks + #make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + #save the phase masks for later use + saveMasks = False + if saveMasks: + np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1) + np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2) + + print("All plots saved to results/ folder.") + +def reset_globals(): + obj_values1[:] = [] + obj_values2[:] = [] + opt_history[:] = [] + w_norm[:] = np.zeros((2*S)) #reset the normalized phase masks for the next iteration + +def metaprop(x): + #initialize results file if it does not exist + + if not os.path.exists('results/metaprop_results.txt'): + with open('results/metaprop_results.txt', 'w') as f: + f.write("d1 [m] d2 [m] d3 [m] ob1 ob2 t\n") + + m, n = x.shape + C = np.zeros((m,1)) + for i in range(m): + for j in range(n): + d[j] = 1e-6*np.round(x[i,j]) #update the global propagation distances with the new design parameters + + print("Current propagation distances: ", d) + update_phase_factors() #update the phase factors for the new propagation distances + update_input_fields() + metaprop_solve() #solve the inner optimization problem for the given design parameters + + ob1 = obj_values1[-1] + ob2 = obj_values2[-1] + t = opt_history[-1] + #save all the partial results for a specific set of design parameters x to a text file + #in append mode, so that we can keep track of the optimization history + with open('results/metaprop_results.txt', 'a') as f: + f.write(f"{d[0]:.6e} {d[1]:.6e} {d[2]:.6e} {ob1:.6e} {ob2:.6e} {t:.6e}\n") + + alpha = 0.5 + C[i] = t + alpha * np.abs(ob1 - ob2) #cost function to be minimized by EGO) + + reset_globals() + return C + +def run_ego(): + ############################## + #EGO setup + ############################## + n_ego_iter = 30 + xlimits = np.array([[200,700], [200,700], [200,700]]) + seed = 777 + design_space = DesignSpace(xlimits, seed=seed) + criterion = 'EI' + ndoe = 20 + + #build DOE + sampling = LHS(xlimits=xlimits, seed=seed) + xdoe = sampling(ndoe) + + sm = KRG(design_space=design_space, n_start=25, print_global=False) + ego = EGO( + n_iter=n_ego_iter, + criterion=criterion, + xdoe=xdoe, + surrogate=sm, + n_start=25, + ) + + x_opt, y_opt, ind_best, x_data, y_data = ego.optimize(fun=metaprop) + #save the optimization results to a text file + + np.savetxt('results/ego_opt_results.txt', np.hstack((x_opt,y_opt))) + +if __name__ == "__main__": + #if the results folder does not exist, create it + if not os.path.exists('results'): + os.makedirs('results') + run_ego() \ No newline at end of file diff --git a/metaprop_multi_objective/metaprop_fibermode_doublet_multi.py b/metaprop_multi_objective/metaprop_fibermode_doublet_multi.py new file mode 100644 index 0000000..4bddcc9 --- /dev/null +++ b/metaprop_multi_objective/metaprop_fibermode_doublet_multi.py @@ -0,0 +1,676 @@ +import time +import multiprocessing + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +import os + +############################## +#setup results folder +#if the results folder does not exist, create it + +if not os.path.exists("results"): + os.makedirs("results") + +############################## +#physics and simulation domain +wavelength = 1.55e-6 +k0 = 2 * np.pi / wavelength + +opt_max_eval = 300 + +unit_cell_pitch = 400e-9 #equivalent to spatial sampling + +Nx = 2048 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d0 = 345e-6 #propagation distance: source - MS1 +d1 = 500e-6 #propagation distance: MS1 - MS2 +d2 = 300e-6 #propagation distance: MS2 - target +d = [d1, d2] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target + +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#Source +beam_waist = 4e-6 +source_spacing = 300e-6 +############################## + +############################## +#PYFFTW PLANNING SETUP +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = k0 * d0 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_0 = np.exp(1j * phase_factor_0) +P_0_nat = np.fft.ifftshift(P_0) +phase_factor_1 = k0 * d1 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_1 = np.exp(1j * phase_factor_1) +P_1_nat = np.fft.ifftshift(P_1) +phase_factor_2 = k0 * d2 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_2 = np.exp(1j * phase_factor_2) +P_2_nat = np.fft.ifftshift(P_2) + +P_1_dagger = P_1.T.conj() +P_2_dagger = P_2.T.conj() +P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj() +P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj() +############################## + +############################## +#Plot zoom parameters [um] +zoom_x = 350 +zoom_core_diameter = 2 * 6 +zoom_x_outmode = 12 +zoom_y = 350 +zoom_y_outmode = 50 +full_view_x = size_x * 1e6 / 2 +full_view_y = size_y * 1e6 / 2 +############################## + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(mode_list:list)->list: + '''Returns the fiber mode pattern for the selected n index. + Args: + mode_list: the list of the selected fiber mode indices + Returns: + The fiber mode patterns corresponding to the selected indices returned + as a list of 2D arrays with the same order as the input mode_list + ''' + print("Computing fiber patterns for mode indices: ", mode_list) + lda = 1.55e-6 + + n_co = 1.4630 #core refractive index + n_cl = 1.4585 #cladding refractive index + a = 6e-6 #fiber core radius + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + + b_solutions_at_V_obs = [] + for n in mode_list: + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + output_patterns = [] + for enum_index,n_mode_sel in enumerate(mode_list): + b_mode_sel = b_solutions_at_V_obs[enum_index] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n_mode_sel,chi_co * r) / jv(n_mode_sel, chi_co * a) * np.cos(n_mode_sel * phi) + else:#outside core + E_field[i,j] = kv(n_mode_sel, chi_cl * r) / kv(n_mode_sel, chi_cl * a) * np.cos(n_mode_sel * phi) + + output_patterns.append(E_field) + return output_patterns + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates. + Normalization consists in scaling the field such that its integrated intensity is 1. + + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The 2D normalized Gaussian source field. + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + + return gaussian_field_normalized + +def propagate_source(source_to_propagate): + '''Returns the propagated source field (flattened) just before metasurface 1 (MS1). + This involves propagating the source for a distance d0: from its plane of definition up to MS1. + Args: + source_to_propagate: the source field to be propagated (flattened) + Returns: + The propagated source field (flattened) just before MS1. + ''' + + fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten() + + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + plt.plot(x_axis[i], y_axis[i]) + plt.xlabel("x [um]") + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None: + '''Makes a polar plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_r: zoom level for the radial axis (in micrometers) + Returns: + None + ''' + fig, ax = plt.subplots(figsize=(6, 6)) + if choose_quantity == "amplitude": + given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting + im = ax.imshow(given_field_2d,origin='lower') + elif choose_quantity == "phase": + given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting + im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + center_x, center_y = Nx//2, Ny//2 + radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels + circle = patches.Circle((center_x, center_y), radius, transform=ax.transData) + im.set_clip_path(circle) + ax.set_xlim(center_x - radius, center_x + radius) + ax.set_ylim(center_y - radius, center_y + radius) + ax.axis('off') + cbar = plt.colorbar(im, ax=ax) + plt.savefig("results/" + save_name + ".pdf") + + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the simulation spatial grid by a given physical (true space) amount. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X + shift_amount_x + Yp = Y + shift_amount_y + return Xp, Yp + +########################### +#initialize source field +source_field_1 = get_source_field(beam_waist, center_x=-source_spacing/2, center_y=0) +source_field_2 = get_source_field(beam_waist, center_x=source_spacing/2, center_y=0) + +#source field integrated intensity +source_1_power = np.sum(np.abs(source_field_1)**2) +source_2_power = np.sum(np.abs(source_field_2)**2) +print("Source 1 field integrated intensity: %.4e" % source_1_power) +print("Source 2 field integrated intensity: %.4e" % source_2_power) +#make a plot and save the source field +make_2Dplot_of(source_field_1, choose_quantity="amplitude", save_name="source_1_field_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) +make_2Dplot_of(source_field_1, choose_quantity="phase", save_name="source_1_field_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) +make_2Dplot_of(source_field_2, choose_quantity="amplitude", save_name="source_2_field_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) +make_2Dplot_of(source_field_2, choose_quantity="phase", save_name="source_2_field_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) +########################### + +########################### +#initialize input field 1 (propagated source 1 field) +input_field_1 = propagate_source(source_field_1) +input_field_1_2d = input_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting + +#initialize input field 2 (propagated source 2 field) +input_field_2 = propagate_source(source_field_2) +input_field_2_2d = input_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting + +#input field 1 integrated intensity +input_field_1_intensity = np.sum(np.abs(input_field_1)**2) +print("Input field integrated intensity: %.4e" % input_field_1_intensity) + +#input field 2 integrated intensity +input_field_2_intensity = np.sum(np.abs(input_field_2)**2) +print("Input field integrated intensity: %.4e" % input_field_2_intensity) + +#make a plot and save the input field +make_2Dplot_of(input_field_1_2d, choose_quantity="amplitude", save_name="input_field_1_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(input_field_1_2d, choose_quantity="phase", save_name="input_field_1_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(input_field_2_2d, choose_quantity="amplitude", save_name="input_field_2_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(input_field_2_2d, choose_quantity="phase", save_name="input_field_2_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) + +#list of input fields to be propagated within the optimization loop +input_field_list = [input_field_1, input_field_2] +########################### + +########################### +#initialize target field +target_mode_indices = [1,2] +fiber_mode_pattern_1, fiber_mode_pattern_2 = get_fiber_mode_pattern(target_mode_indices) + +target_Efield_1_2d = fiber_mode_pattern_1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_1)**2)) +target_Efield_1 = target_Efield_1_2d.flatten() #normalized target field (intensity = 1) + +target_Efield_2_2d = fiber_mode_pattern_2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_2)**2)) +target_Efield_2 = target_Efield_2_2d.flatten() #normalized target field (intensity = 1) + +# Plot target fields using polar coordinates +make_polarPlot_of(target_Efield_1_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_core_diameter) +make_polarPlot_of(target_Efield_1_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_core_diameter) +make_polarPlot_of(target_Efield_2_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude_polar", plot_zoom_r=zoom_core_diameter) +make_polarPlot_of(target_Efield_2_2d, choose_quantity="phase", save_name="target_field_2_phase_polar", plot_zoom_r=zoom_core_diameter) + +target_field_list = [target_Efield_1, target_Efield_2] +########################### + +def forward_propagate(input_field:np.ndarray,weights:np.ndarray) -> np.ndarray: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + the output field computed at the observation plane + ''' + print("Starting propagating...") + intermediate_fields = [None, None] + start_time = time.time() + phase_mask = phase_given_w(weights) + fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + #FIRST PROPAGATION + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy()) + #print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2)) + + fft_input_array[:,:] = np.fft.ifftshift((np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny))))) + fft_operator() + + #SECOND PROPAGATION + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy()) + + end_time = time.time() + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + return intermediate_fields + +def adjoint_propagate(input_field_2d,target_Efield_2d,intermediate_fields,input_weights): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradients of the cost function with respect to the phase mask parameters (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + phase_mask = phase_given_w(input_weights) + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger) + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + + +def value_and_grad(weights): + ''' + Returns the value of the cost function and its gradient with respect to the phase mask parameters for + every available set of input/target fields. + Args: + weights: the current phase mask parameters (flattened) + Returns: + values: the value of the cost function for each input/target field pair + gradients: the gradient of the cost function with respect to the phase mask parameters for each input/target field pair + ''' + + values = [] + gradients = [] + + phase_mask[:] = phase_given_w(weights) #update phase mask in place + for i, input_field in enumerate(input_field_list): + + intermediate_fields = forward_propagate(input_field, weights) + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(intermediate_fields[-1].flatten() * np.conj(target_field_list[i]))) ** 2 + C = np.real(np.sum(C_s)) + + values.append(C) + gradients.append( + adjoint_propagate( + input_field_2d = input_field.reshape((Nx, Ny)), + target_Efield_2d = target_field_list[i].reshape((Nx, Ny)), + intermediate_fields = intermediate_fields, + input_weights = weights + ) + ) + + return np.array(values), np.array(gradients) + +opt_history = [] +opt_history1 = [] +opt_history2 = [] +def obj_fun(weights, grad): + ''' + Objective function for the optimization. Computes the overall cost function by taking the mean value + of the individual cost functions for each input/target field pair. Also computes the gradient of the overall cost function + with respect to the weights parameters. + Args: + weights: the current normalized phase mask parameters (flattened) + grad: the gradient of the overall cost function with respect to the weights parameters (flattened) + Returns: + average_obj_val: the average value of the cost function across all input/target field pairs + ''' + w_norm[:] = weights + + obj_val, grads = value_and_grad(weights) + + + average_obj_val = np.mean(obj_val) + + if grads.size > 0: + target_num = len(target_field_list) + + if target_num > 1: + grads_sum = grads[0] + grads[1] + else: + grads_sum = grads[0] + + grad[:] = 1 / target_num * (grads_sum) * 2 * np.pi + #make a plot of the magnitude of the gradient + make_2Dplot_of(np.abs(grad[S:].reshape((Nx, Ny))), choose_quantity="amplitude", save_name="gradient_magnitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + opt_history.append(average_obj_val) + opt_history1.append(obj_val[0]) + opt_history2.append(obj_val[1]) + make_1Dplot_of([np.arange(len(opt_history))]*3, [opt_history,opt_history1,opt_history2], plot_zoom_x=len(opt_history), save_name="optimization_history") + + return average_obj_val + + +def initialize_masks(): + X1,Y1 = shift_spatial_grid(-source_spacing/2,0) + rho1 = np.sqrt(X1**2 + Y1**2) + circular_mask_1 = np.where(rho1.flatten() < 3*beam_waist, 1, 0) + + X2,Y2 = shift_spatial_grid(source_spacing/2,0) + rho2 = np.sqrt(X2**2 + Y2**2) + circular_mask_2 = np.where(rho2.flatten() < 3*beam_waist, 1, 0) + + target_pattern_1_norm_phase = (np.angle(target_Efield_1) + 2 * np.pi) % (2 * np.pi) / (2*np.pi) + target_pattern_1_norm_phase = np.roll(target_pattern_1_norm_phase, shift=int(source_spacing/sampling_period/2), axis=0) + + target_pattern_2_norm_phase = (np.angle(target_Efield_2) + 2 * np.pi) % (2 * np.pi) / (2*np.pi) + target_pattern_2_norm_phase = np.roll(target_pattern_2_norm_phase, shift=-int(source_spacing/sampling_period/2), axis=0) + #on metasurface 1, set initial guess on normalized phase elements + w_norm[:S] += circular_mask_1 * target_pattern_1_norm_phase + circular_mask_2 * target_pattern_2_norm_phase + + + +if __name__ == "__main__": + ########################### + #initialize the normalized phase masks and global phase mask + w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks + initialize_masks() + phase_mask = phase_given_w(w_norm) + + #make an initial plot of the phase masks + make_2Dplot_of(phase_mask[:S].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(phase_mask[S:].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + #define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. + #the bounding box is the outer shell of a square with thickness 100 um + norm_phase_min = 0.0 + norm_phase_max = 1.0 + mask_buffer = 100e-6 + mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) + mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) + mask_lower_bounds = np.zeros_like(mask_upper_bounds) + + ########################### + + #initialize nlopt solver + solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S) #weights (2S) + epigraph (1) + weights_lower_bounds = mask_lower_bounds.flatten() + weights_upper_bounds = mask_upper_bounds.flatten() + solver.set_lower_bounds(weights_lower_bounds) + solver.set_upper_bounds(weights_upper_bounds) + solver.set_max_objective(obj_fun) + + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-7) + solver.set_param("verbosity",1) + + print("Starting optimization...") + start = True + if start: + w_norm[:] = solver.optimize(w_norm) + print("Optimization completed.") + + #verify the results + opt_weights = w_norm + if len(input_field_list) > 1: + verify_out_field_1 = forward_propagate(input_field_list[0], opt_weights)[-1] + verify_out_field_2 = forward_propagate(input_field_list[1], opt_weights)[-1] + all_source_on_field = input_field_list[0] + input_field_list[1] + verify_out_field_all = forward_propagate(all_source_on_field, opt_weights)[-1] + + make_polarPlot_of(verify_out_field_1, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source1_amplitude", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_1, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source1_phase", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_2, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source2_amplitude", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_2, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source2_phase", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_all, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_allSource_amplitude", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_all, choose_quantity="phase", save_name="modeconv1_optimized_output_field_allSource_phase", plot_zoom_r=zoom_core_diameter) + + #make 1D slices of target/output fields to compare + slice_y_index = Ny//2 + slice_x = xs * 1e6 + slice_target_1 = target_Efield_1.reshape((Nx, Ny))[slice_y_index,:] + slice_target_2 = target_Efield_2.reshape((Nx, Ny))[slice_y_index,:] + slice_output_1 = verify_out_field_1.reshape((Nx, Ny))[slice_y_index,:] + slice_output_2 = verify_out_field_2.reshape((Nx, Ny))[slice_y_index,:] + slice_output_all = verify_out_field_all.reshape((Nx, Ny))[slice_y_index,:] + #amplitude and phase target 1 + make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target_1), np.abs(slice_output_1)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_amplitude") + make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target_1)+2*np.pi)%(2*np.pi), (np.angle(slice_output_1)+2*np.pi)%(2*np.pi)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_phase") + #amplitude and phase target 2 + make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target_2), np.abs(slice_output_2)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source2_amplitude") + make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target_2)+2*np.pi)%(2*np.pi), (np.angle(slice_output_2)+2*np.pi)%(2*np.pi)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source2_phase") + + else: + verify_out_field_1 = forward_propagate(input_field_list[0], opt_weights)[-1] + make_2Dplot_of(verify_out_field_1, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source1_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) + make_2Dplot_of(verify_out_field_1, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source1_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) + + #make a slice of the target/output fields to compare + slice_y_index = Ny//2 + slice_x = xs * 1e6 + slice_target_1 = target_Efield_1.reshape((Nx, Ny))[slice_y_index,:] + slice_output_1 = verify_out_field_1.reshape((Nx, Ny))[slice_y_index,:] + #amplitude and phase target 1 + make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target_1), np.abs(slice_output_1)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_amplitude") + make_1Dplot_of([slice_x, slice_x], [np.angle(slice_target_1), np.angle(slice_output_1)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_phase") + + #save the output fields for later use + saveFields = True + if saveFields: + np.savetxt("results/modeconv1_optimized_output_field_source1.txt", verify_out_field_1.reshape((Nx, Ny))) + np.savetxt("results/modeconv1_optimized_output_field_source2.txt", verify_out_field_2.reshape((Nx, Ny))) + np.savetxt("results/modeconv1_optimized_output_field_allSource.txt", verify_out_field_all.reshape((Nx, Ny))) + + # Reshape fields to 2D for visualization + phase_mask_1 = phase_given_w(opt_weights[:S]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(opt_weights[S:]).reshape((Nx, Ny)) + # Plot: Phase masks + make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + #save the phase masks for later use + saveMasks = True + if saveMasks: + np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1) + np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2) + + print("All plots saved to results/ folder.") \ No newline at end of file diff --git a/metaprop_multi_objective/metaprop_fibermode_doublet_multi_ego.py b/metaprop_multi_objective/metaprop_fibermode_doublet_multi_ego.py new file mode 100644 index 0000000..f224eb6 --- /dev/null +++ b/metaprop_multi_objective/metaprop_fibermode_doublet_multi_ego.py @@ -0,0 +1,799 @@ +import time +import multiprocessing + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +from smt.applications.ego import EGO, KRG +from smt.sampling_methods import LHS +from smt.design_space import ( + DesignSpace, +) + +import os + +#check results folder exists +if not os.path.exists("results"): + os.makedirs("results") +############################## +#physics and simulation domain +wavelength = 1.55e-6 +k0 = 2 * np.pi / wavelength + +opt_max_eval = 3 + +unit_cell_pitch = 700e-9 #equivalent to spatial sampling + +Nx = 2048 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d = [0, 0, 0] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target +d0 = d[0] #propagation distance: source - MS1 +d1 = d[1] #propagation distance: MS1 - MS2 +d2 = d[2] #propagation distance: MS2 - target + +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#Source +beam_waist = 4e-6 +source_spacing = 0 +############################## + +#Plot zoom parameters [um] +zoom_x = 350 +zoom_core_diameter = 2 * 6 +zoom_x_outmode = 12 +zoom_y = 350 +zoom_y_outmode = 50 +full_view_x = size_x * 1e6 / 2 +full_view_y = size_y * 1e6 / 2 +############################## +#PYFFTW PLANNING SETUP +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_0 = np.exp(1j * phase_factor_0) +P_0_nat = np.fft.ifftshift(P_0) +phase_factor_1 = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_1 = np.exp(1j * phase_factor_1) +P_1_nat = np.fft.ifftshift(P_1) +phase_factor_2 = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_2 = np.exp(1j * phase_factor_2) +P_2_nat = np.fft.ifftshift(P_2) + +P_1_dagger = P_1.T.conj() +P_2_dagger = P_2.T.conj() +P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj() +P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj() +############################## +def update_phase_factors(): + phase_factor_0[:,:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_0[:,:] = np.exp(1j * phase_factor_0) + P_0_nat[:,:] = np.fft.ifftshift(P_0) + phase_factor_1[:,:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_1[:,:] = np.exp(1j * phase_factor_1) + P_1_nat[:,:] = np.fft.ifftshift(P_1) + phase_factor_2[:,:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) + P_2[:,:] = np.exp(1j * phase_factor_2) + P_2_nat[:,:] = np.fft.ifftshift(P_2) + + P_1_dagger[:] = P_1.T.conj() + P_2_dagger[:] = P_2.T.conj() + P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj() + P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj() +############################## +def update_input_fields(): + input_field_1[:] = propagate_source(source_field_1) + input_field_1_2d[:] = input_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting + + #initialize input field 2 (propagated source 2 field) + input_field_2[:] = propagate_source(source_field_2) + input_field_2_2d[:] = input_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting + + #input field 1 integrated intensity + input_field_1_intensity = np.sum(np.abs(input_field_1)**2) + print("Input field integrated intensity: %.4e" % input_field_1_intensity) + + #input field 2 integrated intensity + input_field_2_intensity = np.sum(np.abs(input_field_2)**2) + print("Input field integrated intensity: %.4e" % input_field_2_intensity) + + #make a plot and save the input field + make_2Dplot_of(input_field_1_2d, choose_quantity="amplitude", save_name="input_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(input_field_1_2d, choose_quantity="phase", save_name="input_field_1_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(input_field_2_2d, choose_quantity="amplitude", save_name="input_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(input_field_2_2d, choose_quantity="phase", save_name="input_field_2_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) +############################## + +############################## + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(mode_list:list)->list: + '''Returns the fiber mode pattern for the selected n index. + Args: + mode_list: the list of the selected fiber mode indices + Returns: + The fiber mode patterns corresponding to the selected indices returned + as a list of 2D arrays with the same order as the input mode_list + ''' + #check if the modes in mode_list have already been compute by checking + #the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files. + fiber_modes_input_folder = "fiber_modes" + output_patterns = [] + try: + for enum_index, n in enumerate(mode_list): + #try to load mode index n + try: + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy" + pattern = np.load(pattern_file) + output_patterns.append(pattern) + print("Loaded fiber patterns for mode index: ", n) + #if mode index n was not found, compute it + except FileNotFoundError: + if not os.path.exists(fiber_modes_input_folder): + os.makedirs(fiber_modes_input_folder) + + print("Computing fiber patterns for mode index: ", n) + lda = 1.55e-6 + + n_co = 1.4630 #core refractive index + n_cl = 1.4585 #cladding refractive index + + fiber_core_diameter = 12e-6 # [m] + a = fiber_core_diameter / 2 #fiber core radius + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + + b_solutions_at_V_obs = [] + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + b_mode_sel = b_solutions_at_V_obs[0] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi) + else:#outside core + E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi) + #save the computed pattern to a file for future use + pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy" + np.save(pattern_file, E_field) + output_patterns.append(E_field) + return output_patterns + except: + raise(Exception("Error in computing fiber patterns")) + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates. + Normalization consists in scaling the field such that its integrated intensity is 1. + + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The 2D normalized Gaussian source field. + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + + return gaussian_field_normalized + +def propagate_source(source_to_propagate): + '''Returns the propagated source field (flattened) just before metasurface 1 (MS1). + This involves propagating the source for a distance d0: from its plane of definition up to MS1. + Args: + source_to_propagate: the source field to be propagated (flattened) + Returns: + The propagated source field (flattened) just before MS1. + ''' + + fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten() + + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + plt.plot(x_axis[i], y_axis[i]) + plt.xlabel("x [um]") + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None: + '''Makes a polar plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_r: zoom level for the radial axis (in micrometers) + Returns: + None + ''' + fig, ax = plt.subplots(figsize=(6, 6)) + if choose_quantity == "amplitude": + given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting + im = ax.imshow(given_field_2d,origin='lower') + elif choose_quantity == "phase": + given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting + im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + center_x, center_y = Nx//2, Ny//2 + radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels + circle = patches.Circle((center_x, center_y), radius, transform=ax.transData) + im.set_clip_path(circle) + ax.set_xlim(center_x - radius, center_x + radius) + ax.set_ylim(center_y - radius, center_y + radius) + ax.axis('off') + cbar = plt.colorbar(im, ax=ax) + plt.savefig("results/" + save_name + ".pdf") + plt.close() + + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the simulation spatial grid by a given physical (true space) amount. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X + shift_amount_x + Yp = Y + shift_amount_y + return Xp, Yp + +########################### +#initialize source field +source_field_1 = get_source_field(beam_waist, center_x=-source_spacing/2, center_y=0) +source_field_2 = get_source_field(beam_waist, center_x=source_spacing/2, center_y=0) + +#source field integrated intensity +source_1_power = np.sum(np.abs(source_field_1)**2) +source_2_power = np.sum(np.abs(source_field_2)**2) +print("Source 1 field integrated intensity: %.4e" % source_1_power) +print("Source 2 field integrated intensity: %.4e" % source_2_power) +#make a plot and save the source field +make_2Dplot_of(source_field_1, choose_quantity="amplitude", save_name="source_1_field_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) +make_2Dplot_of(source_field_1, choose_quantity="phase", save_name="source_1_field_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) +make_2Dplot_of(source_field_2, choose_quantity="amplitude", save_name="source_2_field_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) +make_2Dplot_of(source_field_2, choose_quantity="phase", save_name="source_2_field_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) +########################### + +########################### +#initialize input field 1 (propagated source 1 field) +input_field_1 = propagate_source(source_field_1) +input_field_1_2d = input_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting + +#initialize input field 2 (propagated source 2 field) +input_field_2 = propagate_source(source_field_2) +input_field_2_2d = input_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting + +#input field 1 integrated intensity +input_field_1_intensity = np.sum(np.abs(input_field_1)**2) +print("Input field integrated intensity: %.4e" % input_field_1_intensity) + +#input field 2 integrated intensity +input_field_2_intensity = np.sum(np.abs(input_field_2)**2) +print("Input field integrated intensity: %.4e" % input_field_2_intensity) + +#make a plot and save the input field +#make_2Dplot_of(input_field_1_2d, choose_quantity="amplitude", save_name="input_field_1_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +#make_2Dplot_of(input_field_1_2d, choose_quantity="phase", save_name="input_field_1_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +#make_2Dplot_of(input_field_2_2d, choose_quantity="amplitude", save_name="input_field_2_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +#make_2Dplot_of(input_field_2_2d, choose_quantity="phase", save_name="input_field_2_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) + +#list of input fields to be propagated within the optimization loop +input_field_list = [input_field_1, input_field_2] +########################### + +########################### +#initialize target field +target_mode_indices = [2] +fiber_mode_pattern_1, fiber_mode_pattern_2 = get_fiber_mode_pattern(target_mode_indices) + +target_Efield_1_2d = fiber_mode_pattern_1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_1)**2)) +target_Efield_1 = target_Efield_1_2d.flatten() #normalized target field (intensity = 1) + +target_Efield_2_2d = fiber_mode_pattern_2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_2)**2)) +target_Efield_2 = target_Efield_2_2d.flatten() #normalized target field (intensity = 1) + +# Plot target fields using polar coordinates +make_polarPlot_of(target_Efield_1_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_core_diameter) +make_polarPlot_of(target_Efield_1_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_core_diameter) +make_polarPlot_of(target_Efield_2_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude_polar", plot_zoom_r=zoom_core_diameter) +make_polarPlot_of(target_Efield_2_2d, choose_quantity="phase", save_name="target_field_2_phase_polar", plot_zoom_r=zoom_core_diameter) + +target_field_list = [target_Efield_1, target_Efield_2] +########################### + +def forward_propagate(input_field:np.ndarray,weights:np.ndarray) -> np.ndarray: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + the output field computed at the observation plane + ''' + print("Starting propagating...") + intermediate_fields = [None, None] + start_time = time.time() + phase_mask = phase_given_w(weights) + fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))) + fft_operator() + #print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + #FIRST PROPAGATION + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + #print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + #print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy()) + #print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2)) + + fft_input_array[:,:] = np.fft.ifftshift((np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny))))) + fft_operator() + + #SECOND PROPAGATION + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy()) + + end_time = time.time() + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + return intermediate_fields + +def adjoint_propagate(input_field_2d,target_Efield_2d,intermediate_fields,input_weights): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradients of the cost function with respect to the phase mask parameters (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + phase_mask = phase_given_w(input_weights) + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger) + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + + +def value_and_grad(weights): + ''' + Returns the value of the cost function and its gradient with respect to the phase mask parameters for + every available set of input/target fields. + Args: + weights: the current phase mask parameters (flattened) + Returns: + values: the value of the cost function for each input/target field pair + gradients: the gradient of the cost function with respect to the phase mask parameters for each input/target field pair + ''' + + values = [] + gradients = [] + + phase_mask[:] = phase_given_w(weights) #update phase mask in place + for i, input_field in enumerate(input_field_list): + + intermediate_fields = forward_propagate(input_field, weights) + + #circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0) + C_s = np.abs(np.sum(intermediate_fields[-1].flatten() * np.conj(target_field_list[i]))) ** 2 + C = np.real(np.sum(C_s)) + + values.append(C) + gradients.append( + adjoint_propagate( + input_field_2d = input_field.reshape((Nx, Ny)), + target_Efield_2d = target_field_list[i].reshape((Nx, Ny)), + intermediate_fields = intermediate_fields, + input_weights = weights + ) + ) + + return np.array(values), np.array(gradients) + +opt_history = [] +opt_history1 = [] +opt_history2 = [] +def obj_fun(weights, grad): + ''' + Objective function for the optimization. Computes the overall cost function by taking the mean value + of the individual cost functions for each input/target field pair. Also computes the gradient of the overall cost function + with respect to the weights parameters. + Args: + weights: the current normalized phase mask parameters (flattened) + grad: the gradient of the overall cost function with respect to the weights parameters (flattened) + Returns: + average_obj_val: the average value of the cost function across all input/target field pairs + ''' + w_norm[:] = weights + + obj_val, grads = value_and_grad(weights) + average_obj_val = np.mean(obj_val) + + if grads.size > 0: + target_num = len(target_field_list) + + if target_num > 1: + grads_sum = grads[0] + grads[1] + else: + grads_sum = grads[0] + + grad[:] = 1 / target_num * (grads_sum) * 2 * np.pi + #make a plot of the magnitude of the gradient + #make_2Dplot_of(np.abs(grad[S:].reshape((Nx, Ny))), choose_quantity="amplitude", save_name="gradient_magnitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + opt_history.append(average_obj_val) + opt_history1.append(obj_val[0]) + opt_history2.append(obj_val[1]) + make_1Dplot_of([np.arange(len(opt_history))]*3, [opt_history,opt_history1,opt_history2], plot_zoom_x=len(opt_history), save_name="optimization_history") + + return average_obj_val + +def initialize_masks(): + X1,Y1 = shift_spatial_grid(-source_spacing/2,0) + rho1 = np.sqrt(X1**2 + Y1**2) + circular_mask_1 = np.where(rho1.flatten() < 3*beam_waist, 1, 0) + + X2,Y2 = shift_spatial_grid(source_spacing/2,0) + rho2 = np.sqrt(X2**2 + Y2**2) + circular_mask_2 = np.where(rho2.flatten() < 3*beam_waist, 1, 0) + + target_pattern_1_norm_phase = (np.angle(target_Efield_1) + 2 * np.pi) % (2 * np.pi) / (2*np.pi) + target_pattern_1_norm_phase = np.roll(target_pattern_1_norm_phase, shift=int(source_spacing/sampling_period/2), axis=0) + + target_pattern_2_norm_phase = (np.angle(target_Efield_2) + 2 * np.pi) % (2 * np.pi) / (2*np.pi) + target_pattern_2_norm_phase = np.roll(target_pattern_2_norm_phase, shift=-int(source_spacing/sampling_period/2), axis=0) + #on metasurface 1, set initial guess on normalized phase elements + w_norm[:S] += circular_mask_1 * target_pattern_1_norm_phase + circular_mask_2 * target_pattern_2_norm_phase + +#global definitions +w_norm = np.zeros((2*S)) +phase_mask = np.zeros((2*S)) + +def reset_globals(): + #reset global variables before next EGO iteration + opt_history[:] = [] + opt_history1[:] = [] + opt_history2[:] = [] + w_norm[:] = np.zeros((2*S)) + phase_mask[:] = np.zeros((2*S)) + +def metaprop_solve(): + ########################### + #initialize the normalized phase masks and global phase mask + + initialize_masks() + phase_mask[:] = phase_given_w(w_norm) + + #make an initial plot of the phase masks + #make_2Dplot_of(phase_mask[:S].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + #make_2Dplot_of(phase_mask[S:].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + #define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. + #the bounding box is the outer shell of a square with thickness 100 um + norm_phase_min = 0.0 + norm_phase_max = 1.0 + mask_buffer = 100e-6 + mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) + mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) + mask_lower_bounds = np.zeros_like(mask_upper_bounds) + + ########################### + + #initialize nlopt solver + solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S) #weights (2S) + epigraph (1) + weights_lower_bounds = mask_lower_bounds.flatten() + weights_upper_bounds = mask_upper_bounds.flatten() + solver.set_lower_bounds(weights_lower_bounds) + solver.set_upper_bounds(weights_upper_bounds) + solver.set_max_objective(obj_fun) + + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-7) + solver.set_param("verbosity",1) + + print("Starting optimization...") + start = True + if start: + w_norm[:] = solver.optimize(w_norm) + print("Optimization completed.") + + #verify the results + opt_weights = w_norm + if len(input_field_list) > 1: + verify_out_field_1 = forward_propagate(input_field_list[0], opt_weights)[-1] + verify_out_field_2 = forward_propagate(input_field_list[1], opt_weights)[-1] + + make_polarPlot_of(verify_out_field_1, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source1_amplitude", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_1, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source1_phase", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_2, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source2_amplitude", plot_zoom_r=zoom_core_diameter) + make_polarPlot_of(verify_out_field_2, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source2_phase", plot_zoom_r=zoom_core_diameter) + #make 1D slices of target/output fields to compare + slice_y_index = Ny//2 + slice_x = xs * 1e6 + slice_target_1 = target_Efield_1.reshape((Nx, Ny))[slice_y_index,:] + slice_target_2 = target_Efield_2.reshape((Nx, Ny))[slice_y_index,:] + slice_output_1 = verify_out_field_1.reshape((Nx, Ny))[slice_y_index,:] + slice_output_2 = verify_out_field_2.reshape((Nx, Ny))[slice_y_index,:] + #amplitude and phase target 1 + make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target_1), np.abs(slice_output_1)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_amplitude") + make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target_1)+2*np.pi)%(2*np.pi), (np.angle(slice_output_1)+2*np.pi)%(2*np.pi)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_phase") + #amplitude and phase target 2 + make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target_2), np.abs(slice_output_2)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source2_amplitude") + make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target_2)+2*np.pi)%(2*np.pi), (np.angle(slice_output_2)+2*np.pi)%(2*np.pi)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source2_phase") + + else: + verify_out_field_1 = forward_propagate(input_field_list[0], opt_weights)[-1] + make_2Dplot_of(verify_out_field_1, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source1_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) + make_2Dplot_of(verify_out_field_1, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source1_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) + + #make a slice of the target/output fields to compare + slice_y_index = Ny//2 + slice_x = xs * 1e6 + slice_target_1 = target_Efield_1.reshape((Nx, Ny))[slice_y_index,:] + slice_output_1 = verify_out_field_1.reshape((Nx, Ny))[slice_y_index,:] + #amplitude and phase target 1 + make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target_1), np.abs(slice_output_1)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_amplitude") + make_1Dplot_of([slice_x, slice_x], [np.angle(slice_target_1), np.angle(slice_output_1)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_phase") + + #save the output fields for later use + saveFields = False + if saveFields: + np.savetxt("results/modeconv1_optimized_output_field_source1.txt", verify_out_field_1.reshape((Nx, Ny))) + np.savetxt("results/modeconv1_optimized_output_field_source2.txt", verify_out_field_2.reshape((Nx, Ny))) + + # Reshape fields to 2D for visualization + phase_mask_1 = phase_given_w(opt_weights[:S]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(opt_weights[S:]).reshape((Nx, Ny)) + # Plot: Phase masks + make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y) + + #save the phase masks for later use + saveMasks = False + if saveMasks: + np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1) + np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2) + + print("All plots saved to results/ folder.") + +def metaprop(x): + if not os.path.exists('results/metaprop_results.txt'): + with open('results/metaprop_results.txt', 'w') as f: + f.write("d1 [m] d2 [m] d3 [m] ob1 ob2 t\n") + + m, n = x.shape + C = np.zeros((m,1)) + for i in range(m): + for j in range(n): + d[j] = 1e-6*np.round(x[i,j]) #update the global propagation distances with the new design parameters + + d[-1] = d[0] + print("Current propagation distances: ", d) + update_phase_factors() #update the phase factors for the new propagation distances + update_input_fields() + print("USING D=",d) + metaprop_solve() #solve the inner optimization problem for the given design parameters + + ob1 = opt_history1[-1] + ob2 = opt_history2[-1] + avg = opt_history[-1] + #save all the partial results for a specific set of design parameters x to a text file + #in append mode, so that we can keep track of the optimization history + with open('results/metaprop_results.txt', 'a') as f: + f.write(f"{d[0]:.6e} {d[1]:.6e} {d[2]:.6e} {ob1:.6e} {ob2:.6e} {avg:.6e}\n") + + alpha = 0.5 + C[i] = -1*avg + alpha * np.abs(ob1 - ob2) #cost function to be minimized by EGO + + reset_globals() + return C + +def run_ego(): + ############################## + #EGO setup + ############################## + n_ego_iter = 3 + xlimits = np.array([[200,700], [200,700]]) + seed = 777 + design_space = DesignSpace(xlimits, seed=seed) + criterion = 'EI' + ndoe = 2 + + #build DOE + sampling = LHS(xlimits=xlimits, seed=seed) + xdoe = sampling(ndoe) + + sm = KRG(design_space=design_space, n_start=25, print_global=False) + ego = EGO( + n_iter=n_ego_iter, + criterion=criterion, + xdoe=xdoe, + surrogate=sm, + n_start=25, + ) + + x_opt, y_opt, ind_best, x_data, y_data = ego.optimize(fun=metaprop) + #save the optimization results to a text file + np.savetxt('results/ego_opt_results.txt', np.hstack((x_opt,y_opt))) + +if __name__ == "__main__": + run_ego() \ No newline at end of file diff --git a/metaprop_multi_objective/metaprop_fibermode_doublet_shiftSource.py b/metaprop_multi_objective/metaprop_fibermode_doublet_shiftSource.py new file mode 100644 index 0000000..1577e16 --- /dev/null +++ b/metaprop_multi_objective/metaprop_fibermode_doublet_shiftSource.py @@ -0,0 +1,611 @@ +import time +import multiprocessing + +import pyfftw +#pyFFTW setup +n_cpu = multiprocessing.cpu_count() +pyfftw.config.NUM_THREADS = n_cpu +pyfftw.interfaces.cache.enable() +pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' + +import nlopt + +import numpy as np +import matplotlib.pyplot as plt +#set the global font size for all plots +plt.rcParams.update({'font.size': 14}) + +from scipy.special import jv, kv + +############################## +#physics and simulation domain +wavelength = 1.55e-6 +k0 = 2 * np.pi / wavelength + +opt_max_eval = 250 + +unit_cell_pitch = 400e-9 #equivalent to spatial sampling + +Nx = 2048 #pixels per dimension +Ny = Nx +S = Nx * Ny + +size_x = unit_cell_pitch * Nx #actual physical size +size_y = unit_cell_pitch * Ny + +xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx) +ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny) + +X, Y = np.meshgrid(xs, ys) +rho = np.sqrt(X**2 + Y**2) + +sampling_period = xs[1] - xs[0] + +d0 = 300e-6 #propagation distance: source - MS1 +d1 = 500e-6 #propagation distance: MS1 - MS2 +d2 = 300e-6 #propagation distance: MS2 - target +d = [d1, d2] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target + +phase_min = 0 +phase_max = 2 * np.pi +############################## + +############################## +#Source +beam_waist = 4e-6 +source_spacing = 300e-6 +############################## + +############################## +#plan pyfftw objects for forward and inverse FFTs +fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) +ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16) + +fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',)) +############################## + +############################## +#setting up spatial frequencies +ks = 2 * np.pi / sampling_period +kappas = np.arange(-Nx//2, Nx//2) * ks / Nx +KX, KY = np.meshgrid(kappas, kappas) +K_parallel = np.sqrt(KX**2 + KY**2) +nu_parallel = K_parallel / (2 * np.pi) + +phase_factor_0 = k0 * d0 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_0 = np.exp(1j * phase_factor_0) +P_0_nat = np.fft.ifftshift(P_0) +phase_factor_1 = k0 * d1 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_1 = np.exp(1j * phase_factor_1) +P_1_nat = np.fft.ifftshift(P_1) +phase_factor_2 = k0 * d2 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j) +P_2 = np.exp(1j * phase_factor_2) +P_2_nat = np.fft.ifftshift(P_2) + +P_1_dagger = P_1.T.conj() +P_2_dagger = P_2.T.conj() +P_2_conj = P_2.conj() +P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj() +P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj() +############################## + +############################## +#Plot zoom parameters [um] +zoom_x = 200 +zoom_y = 200 +all_view_x = size_x * 1e6 / 2 +all_view_y = size_y * 1e6 / 2 +############################## + +def phase_given_w(w): + '''Returns the phase shift given the design parameters w. + Args: + w: design parameters (flattened) + Returns: + The phase shift corresponding to the geometrical parameters w (flattened)''' + #for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi) + return 2 * np.pi * (w) + +def get_pattern() -> np.ndarray: + '''Returns the flattened target fiber mode pattern''' + pattern_name = 'LP11_field_distribution.txt' + pattern = np.loadtxt('misc/' + pattern_name, dtype=complex) + + return pattern.flatten() + +def get_fiber_mode_pattern(): + '''Returns the target fiber mode pattern''' + print("Computing target fiber mode pattern...") + lda = 1.55e-6 + + n_co = 1.4630 + n_cl = 1.4585 + a = 6e-6 + + #find the v parameter range that corresponds to the 1550 nm wavelength + #v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2) + v_1550 = 4.5 + + b = np.linspace(1e-3, 1-1e-3, 400000) + V_list = np.linspace(2, 5.1, 20) + V_obs = v_1550 + #b, V = np.meshgrid(b_list, V_list) + n_list = [0,1,2] + + b_solutions_at_V_obs = [] + for n in n_list: + b_solutions = [] + for V in V_list: + LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b)) + + + intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten()) + + b_solutions.append(b[intersection_index]) + + LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b)) + RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b)) + b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())]) + + n_mode_sel = 2 + b_mode_sel = b_solutions_at_V_obs[n_mode_sel] + + k_co = 2 * np.pi / lda * n_co + k_cl = 2 * np.pi / lda * n_cl + + beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2) + + chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a + chi_cl = v_1550 * np.sqrt(b_mode_sel) / a + + E_field = np.zeros_like(X, dtype=complex) + + for i in range(Nx): + for j in range(Ny): + r = np.sqrt(X[i,j]**2 + Y[i,j]**2) + if r < 10*a: + phi = np.arctan2(Y[i,j], X[i,j]) + if r < a:#inside core + E_field[i,j] = jv(n_mode_sel,chi_co * r) / jv(n_mode_sel, chi_co * a) * np.cos(n_mode_sel * phi) + else:#outside core + E_field[i,j] = kv(n_mode_sel, chi_cl * r) / kv(n_mode_sel, chi_cl * a) * np.cos(n_mode_sel * phi) + + return E_field + +def shift_spatial_grid(shift_amount_x,shift_amount_y): + '''Shifts the spatial grid by a given amount. This is useful for centering the source field on the metasurface. + Args: + shift_amount_x: amount to shift the grid in x direction (in meters) + shift_amount_y: amount to shift the grid in y direction (in meters) + Returns: + the shifted spatial grid (X, Y)''' + Xp = X - shift_amount_x + Yp = Y - shift_amount_y + return Xp, Yp + +def get_source_field(beam_waist,center_x=0,center_y=0,): + '''Returns a normalized Gaussian source field for a given beam waist. Normalization consists in scaling + the field such that its integrated intensity is 1. + Args: + beam_waist: the waist of the Gaussian beam (in meters) + center_x: the x-coordinate of the center of the Gaussian beam (in meters) + center_y: the y-coordinate of the center of the Gaussian beam (in meters) + Returns: + The normalized Gaussian source field + ''' + + if center_x != 0 or center_y != 0: + X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y) + rho = np.sqrt(X_shifted**2 + Y_shifted**2) + else: + rho = np.sqrt(X**2 + Y**2) + + gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened + gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1 + return gaussian_field_normalized + +def propagate_source(): + '''Returns the propagated source field just before metasurface 1 (MS1). This involves propagating the + source for a distance d0: from its plane of definition up to MS1. + Returns: + The propagated source field just before MS1 (flattened) + ''' + fft_input_array[:,:] = source_field_2d + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_0_nat + ifft_operator() + propagated_source_field = ifft_operator.output_array.flatten() + return propagated_source_field + +def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None: + '''Makes a 1D plot of the given data and saves it to a PDF file. + Args: + x_axis: list of x-axis data (list of 1D arrays) + y_axis: list of y-axis data (list of 1D arrays) + plot_zoom_x: zoom level for the x-axis (in micrometers) + save_name: name of the file to save the plot (without extension) + Returns: + None + ''' + if len(x_axis) != len(y_axis): + raise ValueError("Lists x_axis and y_axis must have the same length.") + plt.figure() + for i in range(len(x_axis)): + plt.plot(x_axis[i], y_axis[i]) + plt.xlabel("x [um]") + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.close() + +def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None: + '''Makes a plot of the given 2D field in a zoomed region. + Saves the plot with the chosen filename to a PDF file. + Args: + given_field: the 2D field to be plotted (flattened) + choose_quantity: either "amplitude" or "phase" to select quantity to plot + save_name: name of the file to save the plot (without extension) + plot_zoom_x: zoom level for the x-axis (in micrometers) + plot_zoom_y: zoom level for the y-axis (in micrometers) + Returns: + None''' + + given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting + plt.figure() + if choose_quantity == "amplitude": + plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2)) + elif choose_quantity == "phase": + plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + elif choose_quantity == "phase_mask": + plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi) + else: + raise ValueError("choose_quantity must be either 'amplitude' or 'phase'") + + plt.xlim(-plot_zoom_x, plot_zoom_x) + plt.ylim(-plot_zoom_y, plot_zoom_y) + plt.xlabel("x [um]") + plt.ylabel("y [um]") + plt.colorbar() + plt.tight_layout() + plt.savefig("results/" + save_name + ".pdf") + plt.close() + +########################### +#initialize source field +source_field = get_source_field(beam_waist,center_x=source_spacing/2,center_y=0) +source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#source field integrated intensity +source_power = np.sum(np.abs(source_field)**2) +print("Source field integrated intensity: %.4e" % source_power) +#make a plot and save the source field +make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +########################### + +########################### +#initialize input field (propagated source field) +input_field = propagate_source() +input_field_2d = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting +#input field integrated intensity +input_field_intensity = np.sum(np.abs(input_field)**2) +print("Input field integrated intensity: %.4e" % input_field_intensity) +#make a plot and save the input field +make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x*2, plot_zoom_y=zoom_y*2) +make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x*2, plot_zoom_y=zoom_y*2) +########################### + +########################### +#initialize target field +fiber_mode_pattern = get_fiber_mode_pattern() +target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2)) +#shift the target field to math the source position +#target_Efield_2d = np.roll(target_Efield_2d, shift=int((source_spacing/2/unit_cell_pitch)), axis=1) #shift the target field to match the source position +target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1) + +# Plot: Target field +make_2Dplot_of(target_Efield_2d, choose_quantity="amplitude", save_name="target_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +make_2Dplot_of(target_Efield_2d, choose_quantity="phase", save_name="target_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y) +########################### + +########################### +#initialize the normalized phase masks and global phase mask +w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks +X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny)) +rho = np.sqrt(X**2 + Y**2) + +norm_phase_min = 0 +norm_phase_max = 1 +mask_buffer = 100e-6 +mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1) +mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) +mask_lower_bounds = np.zeros_like(mask_upper_bounds) + +def backward_propagate(field_to_backpropagate)-> np.ndarray: + ''' Backpropagates the target field up to the second metasurface. + Args: + field_to_backpropagate: the field to be backpropagated (flattened) + Returns: + The backpropagated field at the second metasurface (flattened) + ''' + fft_input_array[:,:] = field_to_backpropagate.reshape((Nx, Ny)) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + backpropagated_field = ifft_operator.output_array.flatten() + + return backpropagated_field + +def initialize_phase_masks(): + '''Initializes the normalized weights, in-place.''' + initialize = True + + if initialize: + #MS1 + #the first metasurface should steer light towards the center of the second metasurface + #steer_angle = np.atan2(source_spacing/2,d1) + steer_angle = 0.0001 + grating_period = wavelength / np.sin(steer_angle) + grating_phase = (2*np.pi - (2 * np.pi / grating_period) * X) % (2 * np.pi) #blazed grating phase + w_norm[:S] = grating_phase.flatten() / (2 * np.pi) #normalized grating phase + + #MS2 + #propagate the source up to just before the second metasurface + field_before_MS1 = input_field + field_after_MS1 = field_before_MS1 * np.exp(1j * phase_given_w(w_norm[:S])) #apply the first phase mask + fft_input_array[:,:] = field_after_MS1.reshape((Nx, Ny)) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + ifft_operator() + field_before_MS2 = ifft_operator.output_array.flatten() #field just before the second metasurface + #make a plot of the field before the second metasurface + make_2Dplot_of(field_before_MS2, choose_quantity="amplitude", save_name="field_before_MS2_amplitude", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y) + make_2Dplot_of(field_before_MS2, choose_quantity="phase", save_name="field_before_MS2_phase", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y) + + backpropagated_target_field = backward_propagate(target_Efield) + #make plots of the backpropagated target field + make_2Dplot_of(backpropagated_target_field, choose_quantity="amplitude", save_name="backpropagated_target_field_amplitude", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y) + make_2Dplot_of(backpropagated_target_field, choose_quantity="phase", save_name="backpropagated_target_field_phase", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y) + + #MS2_phase = (np.angle(backpropagated_target_field) + 2 * np.pi) % (2 * np.pi) - (np.angle(field_before_MS2) + 2 * np.pi) % (2 * np.pi) + #MS2_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi) - (np.angle(field_before_MS2) + 2 * np.pi) % (2 * np.pi) + MS2_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi) + MS2_phase = (MS2_phase + 2 * np.pi) % (2 * np.pi) #make sure the phase is between 0 and 2pi + #normalize phase masks + circular_mask = np.where(rho.flatten() < 20*6e-6, 1, 0) + w_norm[:S] = 0*circular_mask * grating_phase.flatten() / (2 * np.pi) + w_norm[S:] = circular_mask * MS2_phase.flatten() / (2 * np.pi) + +#target_pattern_phase = (np.angle(target_Efield_2d) + 2 * np.pi) % (2 * np.pi) #make sure the phase is between 0 and 2pi +#target_pattern_phase = np.roll(target_pattern_phase, shift=int((source_spacing/2/unit_cell_pitch)), axis=1) #shift the target phase to match the source position + +#initialize phase masks +initialize_phase_masks() +phase_mask = phase_given_w(w_norm) +make_2Dplot_of(phase_mask[:S].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_1", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y) +make_2Dplot_of(phase_mask[S:].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_2", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y) +#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase. +#the bounding box is the outer shell of a square with thickness 100 um + +########################### + +intermediate_fields = [None, None] +def forward_propagate() -> None: + '''Propagates the input field through the system. Stores results in-place within the intermediate_fields list. + Returns: + None + ''' + print("Starting propagating...") + start_time = time.time() + + fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))) + fft_operator() + print("BEFORE") + fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S + print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product + print("AFTER") + + fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S + non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1) + non_propagating_mask = np.fft.ifftshift(non_propagating_mask) + propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S + print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power)) + + ifft_operator() + intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy()) + print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2)) + + fft_input_array[:,:] = np.fft.ifftshift((np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny))))) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product + ifft_operator() + intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy()) + + end_time = time.time() + #plot intermediate fields + make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y) + make_2Dplot_of(intermediate_fields[0], choose_quantity="phase", save_name="intermediate_field_1_phase", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y) + make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y) + make_2Dplot_of(intermediate_fields[1], choose_quantity="phase", save_name="intermediate_field_2_phase", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y) + print("Propagation finished in {:.6f} seconds.".format(end_time - start_time)) + + +def adjoint_propagate(): + '''Backpropagates the output field using the adjoint of the propagation matrix P. + Returns: + the gradient of the cost function with respect to the phase mask (flattened) + ''' + print("Computing adjoint...") + start_time = time.time() + + Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny)) + Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny)) + + #compute the adjoint source + fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d) + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat + ifft_operator() + fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger) + + grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten() + + fft_operator() + ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise + ifft_operator() + + grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten() + + end_time = time.time() + print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time)) + return np.concatenate((grad_C_phi_1, grad_C_phi_2)) + +def _compute_cost(): + + forward_propagate() + + output_field = intermediate_fields[-1].flatten() + + #circular_mask = np.where(rho.flatten() < 20*6e-6, 1, 0) + C_s = np.abs(np.sum(output_field * np.conj(target_Efield))) ** 2 + C_s = np.real(np.sum(C_s)) + + C_t = C_s + + return C_t + +opt_history = [] + +def cost_fun(x, grad): + ''' Cost function to be minimized. + Args: + x: input normalized parameter field (flattened) + grad: gradient of the cost function with respect to x, modified in place (flattened) + Returns: + The cost function value for the input parameter field x. + ''' + w_norm[:] = x + + phase_mask[:] = phase_given_w(w_norm) + + C = _compute_cost() + + opt_history.append(C) + + plt.figure() + plt.plot(opt_history) + plt.xlabel('Iteration') + plt.ylabel('Cost Function Value') + plt.title('Optimization History') + plt.tight_layout() + plt.savefig("results/modeconv1_optimization_history.pdf") + plt.close() + + if grad.size > 0: + grad[:] = adjoint_propagate() * 2 * np.pi + #print the gradient magnitude + grad_magnitude = np.abs(grad[S:]).reshape((Nx, Ny)) + make_2Dplot_of(grad_magnitude, choose_quantity="amplitude", save_name="gradient_magnitude", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y) + return C + +if __name__ == "__main__": + #initialize nlopt solver + solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S) + + solver.set_lower_bounds(mask_lower_bounds.flatten()) + solver.set_upper_bounds(mask_upper_bounds.flatten()) + solver.set_max_objective(cost_fun) + solver.set_maxeval(opt_max_eval) + solver.set_param("dual_ftol_rel", 1e-7) + solver.set_param("verbosity",1) + + print("Starting optimization...") + start = True + if start: + w_norm[:] = solver.optimize(w_norm) + print("Optimization completed.") + + #verify the results + phase_mask = phase_given_w(w_norm) + forward_propagate() + output_field = intermediate_fields[-1] + + input_power = np.sum(np.abs(input_field)**2) + output_power = np.sum(np.abs(output_field)**2) + + print("Norm. input power: %.4e" % input_power) + print("Norm. output power: %.4e" % output_power) + #save the output field for later use + np.savetxt("results/modeconv1_optimized_output_field.txt", output_field.reshape((Nx, Ny))) + # Plot results in individual PDF figures + + # Reshape fields to 2D for visualization + output_field_2d = output_field.reshape((Nx, Ny)) + phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny)) + phase_mask_2 = phase_given_w(w_norm[S:]).reshape((Nx, Ny)) + + a = 6e-6 + d = 10 * a * 1e6 + + # Plot: Output field amplitude and phase + make_2Dplot_of(output_field_2d, choose_quantity="amplitude", save_name="output_field_amplitude", plot_zoom_x=d, plot_zoom_y=d) + make_2Dplot_of(output_field_2d, choose_quantity="phase", save_name="output_field_phase", plot_zoom_x=d, plot_zoom_y=d) + + phase_mask_zoom = 150 + # Plot: Phase mask 1 + make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + # Plot: Phase mask 2 + make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom) + + # Plot: Cuts along y=0 axis - Amplitude comparison + y_center_idx = Ny // 2 + output_amplitude_cut = np.abs(output_field_2d[y_center_idx, :]) / np.max(np.abs(output_field_2d[y_center_idx, :])) + target_amplitude_cut = np.abs(target_Efield_2d[y_center_idx, :]) / np.max(np.abs(target_Efield_2d[y_center_idx, :])) + + fig, ax = plt.subplots(figsize=(12, 6)) + ax.plot(xs*1e6, output_amplitude_cut, label='Output Field', linewidth=2) + ax.plot(xs*1e6, target_amplitude_cut, label='Target Field', linewidth=2) + ax.set_xlabel('x [um]') + ax.set_xlim(-d,d) + ax.set_ylabel('Amplitude') + ax.set_title('Norm. Field Amplitude along y=0 axis') + ax.legend() + ax.grid(True, alpha=0.3) + plt.tight_layout() + plt.savefig("results/amplitude_cut_comparison.pdf") + plt.close() + + # Plot 8: Cuts along y=0 axis - Phase comparison + output_phase_cut = (np.angle(output_field_2d[y_center_idx, :]) + 2*np.pi) % (2*np.pi) + target_phase_cut = (np.angle(target_Efield_2d[y_center_idx, :]) + 2*np.pi) % (2*np.pi) + x_ax = xs*1e6 + make_1Dplot_of([x_ax, x_ax], [output_phase_cut, target_phase_cut], save_name="phase_cut_comparison") + fig, ax = plt.subplots(figsize=(12, 6)) + ax.plot(xs*1e6, output_phase_cut, label='Output Field', linewidth=2) + ax.plot(xs*1e6, target_phase_cut, label='Target Field', linewidth=2) + ax.set_xlabel('x [um]') + ax.set_ylabel('Phase (rad)') + ax.set_xlim(-d,d) + ax.set_title('Field Phase along y=0 axis') + ax.legend() + ax.grid(True, alpha=0.3) + plt.tight_layout() + plt.savefig("results/phase_cut_comparison.pdf") + plt.close() + + # Plot 9: Intermediate field before second phase mask + make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_amplitude", plot_zoom_x=d, plot_zoom_y=d) + + #save the phase masks for later use + np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1) + np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2) + + print("All plots saved to results/ folder.") + diff --git a/unit_cell/results_S21_p700nm_h900nm.txt b/unit_cell/results_S21_p700nm_h900nm.txt new file mode 100644 index 0000000..348e66d --- /dev/null +++ b/unit_cell/results_S21_p700nm_h900nm.txt @@ -0,0 +1,25 @@ +% Model: cyljnderUnjtCell_solved.mph +% Versjon: COMSOL 6.1.0.282 +% Date: Aug 7 2026, 11:04 +% Table: Table 3 - Global Evaluatjon 3 +% tPost (nm) R (nm) lambda0 (µm) S21 (1) +900 50 1.55 -0.845669386929+0.5044362611307165j +900 63.157894736842096 1.55 -0.8251549977113691+0.5387569255954204j +900 76.31578947368419 1.55 -0.7941914472021001+0.5854009666643545j +900 89.47368421052632 1.55 -0.7471355423796626+0.6469515347910716j +900 102.6315789473684 1.55 -0.6739731961216431+0.7260451361240156j +900 115.78947368421052 1.55 -0.5559252634519462+0.8236596878659627j +900 128.94736842105263 1.55 -0.35623973597781955+0.9314716653309569j +900 142.1052631578947 1.55 -0.005446778866258663+0.9995911107511558j +900 155.26315789473682 1.55 0.5624693373844357+0.8192539801083378j +900 168.42105263157896 1.55 0.9716453344468975-0.026879486169522106j +900 181.57894736842104 1.55 0.33229983484123915-0.9109913324745208j +900 194.73684210526312 1.55 -0.5138804677648452-0.8375112454139356j +900 207.89473684210526 1.55 -0.8979812315659875-0.41599951560137755j +900 221.05263157894737 1.55 -0.8666228661809928-0.35839703156981834j +900 234.21052631578942 1.55 -0.9706534677777011+0.1672691323977627j +900 247.36842105263153 1.55 -0.875683679549086+0.4301009457755804j +900 260.52631578947364 1.55 -0.7186563387658285+0.6394472785795947j +900 273.6842105263158 1.55 -0.6152456609406248+0.5634675095025711j +900 286.8421052631579 1.55 -0.3669553952916399+0.7919528013254294j +900 300 1.55 -0.07785011501821787+0.7863645334693915j diff --git a/unit_cell/uc_center.log b/unit_cell/uc_center.log new file mode 100644 index 0000000..bc4a1c0 --- /dev/null +++ b/unit_cell/uc_center.log @@ -0,0 +1,31 @@ +nohup: ignoring input +Rank 1 is processing diameter 0.274 (index 8) +Rank 2 is processing diameter 0.448 (index 16) +Rank 0 is processing diameter 0.100 (index 0) +Rank 0 is processing diameter 0.122 (index 1) +Rank 1 is processing diameter 0.296 (index 9) +Rank 0 is processing diameter 0.143 (index 2) +Rank 1 is processing diameter 0.317 (index 10) +Rank 2 is processing diameter 0.470 (index 17) +Rank 0 is processing diameter 0.165 (index 3) +Rank 1 is processing diameter 0.339 (index 11) +Rank 0 is processing diameter 0.187 (index 4) +Rank 1 is processing diameter 0.361 (index 12) +Rank 2 is processing diameter 0.491 (index 18) +Rank 0 is processing diameter 0.209 (index 5) +Rank 1 is processing diameter 0.383 (index 13) +Rank 2 is processing diameter 0.513 (index 19) +Rank 0 is processing diameter 0.230 (index 6) +Rank 1 is processing diameter 0.404 (index 14) +Rank 2 is processing diameter 0.535 (index 20) +Rank 0 is processing diameter 0.252 (index 7) +Rank 1 is processing diameter 0.426 (index 15) +Rank 2 is processing diameter 0.557 (index 21) +Rank 2 is processing diameter 0.578 (index 22) +Rank 2 is processing diameter 0.600 (index 23) + +Elapsed run time = 61296.2745 s + +Elapsed run time = 61296.2909 s + +Elapsed run time = 61299.1873 s diff --git a/unit_cell/uc_param_sweep.log b/unit_cell/uc_param_sweep.log new file mode 100644 index 0000000..3403920 --- /dev/null +++ b/unit_cell/uc_param_sweep.log @@ -0,0 +1,31 @@ +nohup: ignoring input +Rank 2: Running simulation for diameter 0.4478260869565217 +Rank 1: Running simulation for diameter 0.27391304347826084 +Rank 0: Running simulation for diameter 0.1 +Rank 0: Running simulation for diameter 0.12173913043478261 +Rank 1: Running simulation for diameter 0.29565217391304344 +Rank 0: Running simulation for diameter 0.14347826086956522 +Rank 1: Running simulation for diameter 0.3173913043478261 +Rank 2: Running simulation for diameter 0.4695652173913043 +Rank 0: Running simulation for diameter 0.16521739130434782 +Rank 1: Running simulation for diameter 0.33913043478260874 +Rank 0: Running simulation for diameter 0.18695652173913044 +Rank 1: Running simulation for diameter 0.36086956521739133 +Rank 2: Running simulation for diameter 0.4913043478260869 +Rank 0: Running simulation for diameter 0.20869565217391306 +Rank 1: Running simulation for diameter 0.3826086956521739 +Rank 2: Running simulation for diameter 0.5130434782608696 +Rank 0: Running simulation for diameter 0.23043478260869565 +Rank 1: Running simulation for diameter 0.4043478260869565 +Rank 2: Running simulation for diameter 0.5347826086956522 +Rank 0: Running simulation for diameter 0.25217391304347825 +Rank 1: Running simulation for diameter 0.4260869565217391 +Rank 2: Running simulation for diameter 0.5565217391304348 + +Elapsed run time = 5816.1358 s +Rank 2: Running simulation for diameter 0.5782608695652174 +Rank 2: Running simulation for diameter 0.6 + +Elapsed run time = 10194.4932 s + +Elapsed run time = 10198.0417 s diff --git a/unit_cell/unit_cell_center_1550_h900nm_res50_wvl_diameters.npy b/unit_cell/unit_cell_center_1550_h900nm_res50_wvl_diameters.npy new file mode 100644 index 0000000..b46d980 Binary files /dev/null and b/unit_cell/unit_cell_center_1550_h900nm_res50_wvl_diameters.npy differ diff --git a/unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_phase.npy b/unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_phase.npy new file mode 100644 index 0000000..4054712 Binary files /dev/null and b/unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_phase.npy differ diff --git a/unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_tran.npy b/unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_tran.npy new file mode 100644 index 0000000..9e7bb1f Binary files /dev/null and b/unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_tran.npy differ diff --git a/unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_wvl.npy b/unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_wvl.npy new file mode 100644 index 0000000..b58573e Binary files /dev/null and b/unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_wvl.npy differ diff --git a/unit_cell/unit_cell_center_wvl.py b/unit_cell/unit_cell_center_wvl.py new file mode 100644 index 0000000..31f62f7 --- /dev/null +++ b/unit_cell/unit_cell_center_wvl.py @@ -0,0 +1,169 @@ +import meep as mp +mp.verbosity(0) + +import numpy as np +import matplotlib.pyplot as plt + +from mpi4py import MPI + +comm = MPI.COMM_WORLD +rank = comm.Get_rank() +size = comm.Get_size() + +resolution = 80 # pixels/unit length + +wvl_min = 1.5 +wvl_max = 1.6 +wvl_cen = 0.5*(wvl_min+wvl_max) +fmin = 1/wvl_max +fmax = 1/wvl_min +fcen = 1/wvl_cen +df = fmax-fmin +nfreq = 1 + +dpml = 1.0 #PML thickness +dsub = 1.5 + wvl_cen #substrate thickness +dpad = dsub #padding between cell and PML + +k_point = mp.Vector3(0,0,0) #used for PBC + +n_si = 3.45 #silicon refractive index +si = mp.Medium(index=n_si) +n_sio2 = 1.45 +sio2 = mp.Medium(index=n_sio2) +n_air = 1.0 #air refractive index +air = mp.Medium(index=n_air) + +P = 0.7 #unit cell pitch +h = 0.9 #pillar height + +def unitCell_paramsweep(D): + sim_x_size = dpml + dsub + dpad + dpml + sim_z_size = P + sim_y_size = P + + cell_size = mp.Vector3(sim_x_size, sim_y_size, sim_z_size) + + source_position = mp.Vector3(-wvl_cen,0,0) #source position in the middle of the substrate + source = mp.Source( + mp.GaussianSource(fcen, fwidth=df), + component = mp.Ez, + center = source_position, + size = mp.Vector3(0, sim_y_size, sim_z_size), + ) + + pml_layers = [mp.PML(dpml,direction=mp.X)] + + monitor_position = mp.Vector3(h + wvl_cen,0,0) + obs_point = monitor_position + stop_cond = mp.stop_when_fields_decayed(50, mp.Ez, obs_point, 1e-9) + #stop_cond = 10 + sim = mp.Simulation( + resolution = resolution, + cell_size = cell_size, + boundary_layers = pml_layers, + k_point = k_point, + default_material = sio2, + sources = [source], + ) + + flux_mon = sim.add_flux( + [fcen], + mp.FluxRegion(center=monitor_position, size=mp.Vector3(0, sim_y_size, sim_z_size)) + ) + + sim.run(until_after_sources=stop_cond) + input_flux = mp.get_fluxes(flux_mon) + + sim.reset_meep() + + pillar_geo = mp.Cylinder( + material = si, + radius = D/2, + height = h, + center = mp.Vector3(h/2,0,0), + axis=mp.Vector3(1,0,0), + ) + + substrate_geo = mp.Block( + material = sio2, + size = mp.Vector3(dsub+dpml, mp.inf, mp.inf), + center = mp.Vector3(-(dsub+dpml)/2,0,0), + ) + + geometries = [substrate_geo, pillar_geo] + + sim = mp.Simulation( + resolution = resolution, + cell_size = cell_size, + boundary_layers = pml_layers, + geometry = geometries, + k_point = k_point, + sources = [source], + ) + + mode_mon = sim.add_flux( + [fcen], + mp.FluxRegion(center=monitor_position, size=mp.Vector3(0, sim_y_size, sim_z_size)) + ) + + sim.run(until_after_sources=stop_cond) + + freqs = mp.get_eigenmode_freqs(mode_mon) + res = sim.get_eigenmode_coefficients( + mode_mon, [1], eig_parity=mp.ODD_Z + mp.EVEN_Y + ) + coeffs = res.alpha + + mode_wvl = [1/freqs[nf] for nf in range(nfreq)] + mode_tran = [abs(coeffs[0,nf,0])**2 / input_flux[nf] for nf in range(nfreq)] + mode_phase = [np.angle(coeffs[0,nf,0]) for nf in range(nfreq)] + + return mode_wvl, mode_tran, mode_phase + +if rank == 0: + diameters = np.linspace(0.1, 0.6, 24) +else: + diameters = None +diameters = comm.bcast(diameters, root=0) + +local_diameters = np.array_split(diameters, size)[rank] +local_indices = np.array_split(np.arange(len(diameters)), size)[rank] + +local_mode_tran_arr = np.zeros(len(diameters)) +local_mode_phase_arr = np.zeros(len(diameters)) + +for local_i, (global_i, D) in enumerate(zip(local_indices, local_diameters)): + print(f"Rank {rank} is processing diameter {D:.3f} (index {global_i})") + mode_wvl, mode_tran, mode_phase = unitCell_paramsweep(D) + local_mode_tran_arr[global_i] = mode_tran[0] + local_mode_phase_arr[global_i] = mode_phase[0] + +comm.Barrier() +mode_tran_arr = comm.gather(local_mode_tran_arr, root=0) +mode_phase_arr = comm.gather(local_mode_phase_arr, root=0) +final_mode_tran_arr = np.zeros(len(diameters)) +final_mode_phase_arr = np.zeros(len(diameters)) +if rank == 0: + for i in range(size): + final_mode_tran_arr += mode_tran_arr[i] + final_mode_phase_arr += mode_phase_arr[i] + + + np.save("unit_cell_center_wvl_diameters.npy", diameters) + np.save("unit_cell_center_wvl_mode_wvl.npy", mode_wvl) + np.save("unit_cell_center_wvl_mode_tran.npy", final_mode_tran_arr) + np.save("unit_cell_center_wvl_mode_phase.npy", final_mode_phase_arr) + + plt.figure() + plt.subplot(1,2,1) + plt.plot(diameters,final_mode_tran_arr) + plt.xlabel("Pillar Diameter (um)") + plt.ylabel("Transmission") + plt.subplot(1,2,2) + plt.plot(diameters,final_mode_phase_arr) + plt.xlabel("Pillar Diameter (um)") + plt.ylabel("Phase (rad)") + plt.subplots_adjust(wspace=0.5) + plt.savefig("unit_cell_fcen.pdf") + diff --git a/unit_cell/unit_cell_center_wvl_diameters.npy b/unit_cell/unit_cell_center_wvl_diameters.npy new file mode 100644 index 0000000..b46d980 Binary files /dev/null and b/unit_cell/unit_cell_center_wvl_diameters.npy differ diff --git a/unit_cell/unit_cell_center_wvl_mode_phase.npy b/unit_cell/unit_cell_center_wvl_mode_phase.npy new file mode 100644 index 0000000..9b80b06 Binary files /dev/null and b/unit_cell/unit_cell_center_wvl_mode_phase.npy differ diff --git a/unit_cell/unit_cell_center_wvl_mode_tran.npy b/unit_cell/unit_cell_center_wvl_mode_tran.npy new file mode 100644 index 0000000..99e1d66 Binary files /dev/null and b/unit_cell/unit_cell_center_wvl_mode_tran.npy differ diff --git a/unit_cell/unit_cell_center_wvl_mode_wvl.npy b/unit_cell/unit_cell_center_wvl_mode_wvl.npy new file mode 100644 index 0000000..b58573e Binary files /dev/null and b/unit_cell/unit_cell_center_wvl_mode_wvl.npy differ diff --git a/unit_cell/unit_cell_fcen.pdf b/unit_cell/unit_cell_fcen.pdf new file mode 100644 index 0000000..0793b48 Binary files /dev/null and b/unit_cell/unit_cell_fcen.pdf differ diff --git a/unit_cell/unit_cell_fcen_h900nm_res50.pdf b/unit_cell/unit_cell_fcen_h900nm_res50.pdf new file mode 100644 index 0000000..a916085 Binary files /dev/null and b/unit_cell/unit_cell_fcen_h900nm_res50.pdf differ diff --git a/unit_cell/unit_cell_meep.ipynb b/unit_cell/unit_cell_meep.ipynb new file mode 100644 index 0000000..2122207 --- /dev/null +++ b/unit_cell/unit_cell_meep.ipynb @@ -0,0 +1,902 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 6, + "id": "094bf901", + "metadata": {}, + "outputs": [], + "source": [ + "import meep as mp\n", + "import numpy as np\n", + "import numpy.matlib\n", + "import matplotlib.pyplot as plt\n", + "\n", + "resolution = 50 # pixels/unit length\n", + "\n", + "wvl_min = 1.5\n", + "wvl_max = 1.6\n", + "wvl_cen = 0.5*(wvl_min+wvl_max)\n", + "fmin = 1/wvl_max\n", + "fmax = 1/wvl_min\n", + "fcen = 0.5*(fmin+fmax)\n", + "df = fmax-fmin\n", + "nfreq = 10\n", + "\n", + "dpml = 1.0 #PML thickness\n", + "dsub = 1.5 + wvl_cen #substrate thickness\n", + "dpad = dsub #padding between cell and PML\n", + "\n", + "k_point = mp.Vector3(0,0,0) #used for PBC\n", + "\n", + "n_si = 3.45 #silicon refractive index\n", + "si = mp.Medium(index=n_si)\n", + "n_sio2 = 1.45\n", + "sio2 = mp.Medium(index=n_sio2)\n", + "n_air = 1.0 #air refractive index\n", + "air = mp.Medium(index=n_air)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "35f94716", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-----------\n", + "Initializing structure...\n", + "time for choose_chunkdivision = 6.38962e-05 s\n", + "Working in 3D dimensions.\n", + "Computational cell is 8.1 x 0.7 x 0.7 with resolution 50\n", + "time for set_epsilon = 0.787619 s\n", + "-----------\n", + "on time step 535 (time=5.35), 0.0074768 s/step\n", + "on time step 1171 (time=11.71), 0.00629888 s/step\n", + "on time step 1829 (time=18.29), 0.00608258 s/step\n", + "on time step 2488 (time=24.88), 0.00607308 s/step\n", + "on time step 3145 (time=31.45), 0.00609221 s/step\n", + "on time step 3807 (time=38.07), 0.00604308 s/step\n", + "on time step 4467 (time=44.67), 0.00606241 s/step\n", + "field decay(t = 50.01): 5.426741164771886e-06 / 5.426741164771886e-06 = 1.0\n", + "on time step 5127 (time=51.27), 0.00606334 s/step\n", + "on time step 5738 (time=57.38), 0.00655712 s/step\n", + "on time step 6373 (time=63.73), 0.00630479 s/step\n", + "on time step 7031 (time=70.31), 0.0060838 s/step\n", + "on time step 7662 (time=76.62), 0.00634715 s/step\n", + "on time step 8302 (time=83.02), 0.00625346 s/step\n", + "on time step 8959 (time=89.59), 0.00609081 s/step\n", + "on time step 9619 (time=96.19), 0.00606273 s/step\n", + "field decay(t = 100.01): 0.035570203188044235 / 0.035570203188044235 = 1.0\n", + "on time step 10261 (time=102.61), 0.00623869 s/step\n", + "on time step 10919 (time=109.19), 0.00608189 s/step\n", + "on time step 11566 (time=115.66), 0.00618257 s/step\n", + "on time step 12213 (time=122.13), 0.00618699 s/step\n", + "on time step 12863 (time=128.63), 0.00615897 s/step\n", + "on time step 13494 (time=134.94), 0.00634203 s/step\n", + "on time step 14147 (time=141.47), 0.00612982 s/step\n", + "on time step 14800 (time=148), 0.00612841 s/step\n", + "field decay(t = 150.02): 0.11846184324595802 / 0.11846184324595802 = 1.0\n", + "on time step 15422 (time=154.22), 0.00644085 s/step\n", + "on time step 16063 (time=160.63), 0.00624794 s/step\n", + "on time step 16675 (time=166.75), 0.00653603 s/step\n", + "on time step 17263 (time=172.63), 0.00681036 s/step\n", + "on time step 17872 (time=178.72), 0.00656959 s/step\n", + "on time step 18457 (time=184.57), 0.00684303 s/step\n", + "on time step 19107 (time=191.07), 0.00616221 s/step\n", + "on time step 19742 (time=197.42), 0.00630697 s/step\n", + "field decay(t = 200.03): 0.040856206457377704 / 0.11846184324595802 = 0.3448891671603442\n", + "on time step 20403 (time=204.03), 0.00605866 s/step\n", + "on time step 21048 (time=210.48), 0.00620925 s/step\n", + "on time step 21683 (time=216.83), 0.00630889 s/step\n", + "on time step 22327 (time=223.27), 0.00621678 s/step\n", + "on time step 22956 (time=229.56), 0.00636194 s/step\n", + "on time step 23606 (time=236.06), 0.00615597 s/step\n", + "on time step 24245 (time=242.45), 0.00626616 s/step\n", + "on time step 24856 (time=248.56), 0.0065549 s/step\n", + "field decay(t = 250.04): 7.321616676715807e-06 / 0.11846184324595802 = 6.180569604606101e-05\n", + "on time step 25501 (time=255.01), 0.00621073 s/step\n", + "on time step 26153 (time=261.53), 0.00614292 s/step\n", + "on time step 26804 (time=268.04), 0.00614698 s/step\n", + "on time step 27455 (time=274.55), 0.00614757 s/step\n", + "on time step 28100 (time=281), 0.0062026 s/step\n", + "on time step 28757 (time=287.57), 0.00609292 s/step\n", + "on time step 29419 (time=294.19), 0.00604787 s/step\n", + "field decay(t = 300.04): 2.0130821207087206e-16 / 0.11846184324595802 = 1.69935066477822e-15\n", + "run 0 finished at t = 300.04 (30004 timesteps)\n", + "-----------\n", + "Initializing structure...\n", + "time for choose_chunkdivision = 7.10487e-05 s\n", + "Working in 3D dimensions.\n", + "Computational cell is 8.1 x 0.7 x 0.7 with resolution 50\n", + " block, center = (-2.025,0,0)\n", + " size (4.05,1e+20,1e+20)\n", + " axes (1,0,0), (0,1,0), (0,0,1)\n", + " dielectric constant epsilon diagonal = (2.1025,2.1025,2.1025)\n", + " cylinder, center = (0.4,0,0)\n", + " radius 0.1, height 0.8, axis (1, 0, 0)\n", + " dielectric constant epsilon diagonal = (11.9025,11.9025,11.9025)\n", + "time for set_epsilon = 47.9716 s\n", + "-----------\n", + "on time step 473 (time=4.73), 0.0084703 s/step\n", + "on time step 1034 (time=10.34), 0.00714037 s/step\n", + "on time step 1587 (time=15.87), 0.0072438 s/step\n", + "on time step 2154 (time=21.54), 0.0070595 s/step\n", + "on time step 2717 (time=27.17), 0.00711487 s/step\n", + "on time step 3274 (time=32.74), 0.00718785 s/step\n", + "on time step 3855 (time=38.55), 0.00689564 s/step\n", + "on time step 4403 (time=44.03), 0.00731156 s/step\n", + "on time step 4980 (time=49.8), 0.00693329 s/step\n", + "on time step 5522 (time=55.22), 0.00738428 s/step\n", + "on time step 6083 (time=60.83), 0.00714259 s/step\n", + "on time step 6656 (time=66.56), 0.00698555 s/step\n", + "on time step 7221 (time=72.21), 0.00708599 s/step\n", + "on time step 7791 (time=77.91), 0.00701823 s/step\n", + "on time step 8358 (time=83.58), 0.00705761 s/step\n", + "on time step 8942 (time=89.42), 0.00685129 s/step\n", + "on time step 9515 (time=95.15), 0.00698397 s/step\n", + "on time step 10092 (time=100.92), 0.00693597 s/step\n", + "on time step 10670 (time=106.7), 0.00693239 s/step\n", + "on time step 11242 (time=112.42), 0.00700489 s/step\n", + "on time step 11813 (time=118.13), 0.00701277 s/step\n", + "on time step 12375 (time=123.75), 0.0071282 s/step\n", + "on time step 12931 (time=129.31), 0.0072055 s/step\n", + "on time step 13505 (time=135.05), 0.0069798 s/step\n", + "on time step 14034 (time=140.34), 0.00757448 s/step\n", + "on time step 14598 (time=145.98), 0.00709762 s/step\n", + "on time step 15161 (time=151.61), 0.0071103 s/step\n", + "on time step 15721 (time=157.21), 0.00714891 s/step\n", + "on time step 16287 (time=162.87), 0.00707846 s/step\n", + "on time step 16838 (time=168.38), 0.00727186 s/step\n", + "on time step 17409 (time=174.09), 0.00701287 s/step\n", + "on time step 17955 (time=179.55), 0.00732891 s/step\n", + "on time step 18505 (time=185.05), 0.00727459 s/step\n", + "on time step 19043 (time=190.43), 0.00744715 s/step\n", + "on time step 19573 (time=195.73), 0.00755739 s/step\n", + "on time step 20106 (time=201.06), 0.00751135 s/step\n", + "on time step 20662 (time=206.62), 0.00720323 s/step\n", + "on time step 21222 (time=212.22), 0.00715362 s/step\n", + "on time step 21791 (time=217.91), 0.00703573 s/step\n", + "on time step 22324 (time=223.24), 0.00751711 s/step\n", + "on time step 22857 (time=228.57), 0.00751257 s/step\n", + "on time step 23407 (time=234.07), 0.00727724 s/step\n", + "on time step 23960 (time=239.6), 0.00724289 s/step\n", + "on time step 24518 (time=245.18), 0.00717636 s/step\n", + "on time step 25087 (time=250.87), 0.00703826 s/step\n", + "on time step 25662 (time=256.62), 0.00695667 s/step\n", + "on time step 26223 (time=262.23), 0.00713661 s/step\n", + "on time step 26769 (time=267.69), 0.00733718 s/step\n", + "on time step 27323 (time=273.23), 0.00723238 s/step\n", + "on time step 27885 (time=278.85), 0.00712397 s/step\n", + "on time step 28442 (time=284.42), 0.00718542 s/step\n", + "on time step 28997 (time=289.97), 0.00721054 s/step\n", + "on time step 29562 (time=295.62), 0.00708772 s/step\n", + "on time step 30132 (time=301.32), 0.00702608 s/step\n", + "on time step 30678 (time=306.78), 0.00733475 s/step\n", + "on time step 31252 (time=312.52), 0.00697776 s/step\n", + "on time step 31821 (time=318.21), 0.00703846 s/step\n", + "on time step 32379 (time=323.79), 0.00717254 s/step\n", + "on time step 32916 (time=329.16), 0.00745362 s/step\n", + "on time step 33465 (time=334.65), 0.00728729 s/step\n", + "on time step 34009 (time=340.09), 0.00735804 s/step\n", + "on time step 34564 (time=345.64), 0.0072098 s/step\n", + "field decay(t = 350.05): 0.16701829070837584 / 0.16701829070837584 = 1.0\n", + "on time step 35117 (time=351.17), 0.0072443 s/step\n", + "on time step 35664 (time=356.64), 0.00732527 s/step\n", + "on time step 36223 (time=362.23), 0.00715988 s/step\n", + "on time step 36781 (time=367.81), 0.00717286 s/step\n", + "on time step 37350 (time=373.5), 0.00703965 s/step\n", + "on time step 37895 (time=378.95), 0.00734196 s/step\n", + "on time step 38450 (time=384.5), 0.00721765 s/step\n", + "on time step 38956 (time=389.56), 0.00791335 s/step\n", + "on time step 39508 (time=395.08), 0.00724688 s/step\n", + "field decay(t = 400.06): 4.532666916542125e-17 / 0.16701829070837584 = 2.713874568658135e-16\n", + "run 0 finished at t = 400.06 (40006 timesteps)\n", + "MPB solved for frequency_1(0.625,0,0) = 0.625 after 7 iters\n", + "Dominant planewave for band 1: (0.625000,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.62963,0,0) = 0.62963 after 7 iters\n", + "Dominant planewave for band 1: (0.629630,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.634259,0,0) = 0.634259 after 7 iters\n", + "Dominant planewave for band 1: (0.634259,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.638889,0,0) = 0.638889 after 7 iters\n", + "Dominant planewave for band 1: (0.638889,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.643519,0,0) = 0.643519 after 7 iters\n", + "Dominant planewave for band 1: (0.643519,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.648148,0,0) = 0.648148 after 7 iters\n", + "Dominant planewave for band 1: (0.648148,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.652778,0,0) = 0.652778 after 7 iters\n", + "Dominant planewave for band 1: (0.652778,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.657407,0,0) = 0.657407 after 7 iters\n", + "Dominant planewave for band 1: (0.657407,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.662037,0,0) = 0.662037 after 7 iters\n", + "Dominant planewave for band 1: (0.662037,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.666667,0,0) = 0.666667 after 7 iters\n", + "Dominant planewave for band 1: (0.666667,-0.000000,0.000000)\n", + "-----------\n", + "Initializing structure...\n", + "time for choose_chunkdivision = 6.50883e-05 s\n", + "Working in 3D dimensions.\n", + "Computational cell is 8.1 x 0.7 x 0.7 with resolution 50\n", + "time for set_epsilon = 0.773665 s\n", + "-----------\n", + "on time step 543 (time=5.43), 0.00737383 s/step\n", + "on time step 1188 (time=11.88), 0.0062086 s/step\n", + "on time step 1828 (time=18.28), 0.00625388 s/step\n", + "on time step 2464 (time=24.64), 0.00629507 s/step\n", + "on time step 3125 (time=31.25), 0.00605655 s/step\n", + "on time step 3774 (time=37.74), 0.00616392 s/step\n", + "on time step 4424 (time=44.24), 0.0061585 s/step\n", + "field decay(t = 50.01): 5.426741164771886e-06 / 5.426741164771886e-06 = 1.0\n", + "on time step 5069 (time=50.69), 0.00620527 s/step\n", + "on time step 5718 (time=57.18), 0.00616512 s/step\n", + "on time step 6397 (time=63.97), 0.00589511 s/step\n", + "on time step 7053 (time=70.53), 0.0061039 s/step\n", + "on time step 7712 (time=77.12), 0.00607532 s/step\n", + "on time step 8321 (time=83.21), 0.00656928 s/step\n", + "on time step 8961 (time=89.61), 0.00625559 s/step\n", + "on time step 9618 (time=96.18), 0.00609708 s/step\n", + "field decay(t = 100.01): 0.035570203188044235 / 0.035570203188044235 = 1.0\n", + "on time step 10257 (time=102.57), 0.00626534 s/step\n", + "on time step 10908 (time=109.08), 0.00614839 s/step\n", + "on time step 11515 (time=115.15), 0.00659816 s/step\n", + "on time step 12153 (time=121.53), 0.00627378 s/step\n", + "on time step 12804 (time=128.04), 0.0061515 s/step\n", + "on time step 13454 (time=134.54), 0.0061539 s/step\n", + "on time step 14110 (time=141.1), 0.00610339 s/step\n", + "on time step 14765 (time=147.65), 0.00610949 s/step\n", + "field decay(t = 150.02): 0.11846184324595802 / 0.11846184324595802 = 1.0\n", + "on time step 15414 (time=154.14), 0.00616336 s/step\n", + "on time step 16069 (time=160.69), 0.0061144 s/step\n", + "on time step 16706 (time=167.06), 0.00628636 s/step\n", + "on time step 17360 (time=173.6), 0.00612495 s/step\n", + "on time step 17970 (time=179.7), 0.00655798 s/step\n", + "on time step 18617 (time=186.17), 0.00619117 s/step\n", + "on time step 19273 (time=192.73), 0.00610361 s/step\n", + "on time step 19914 (time=199.14), 0.0062457 s/step\n", + "field decay(t = 200.03): 0.040856206457377704 / 0.11846184324595802 = 0.3448891671603442\n", + "on time step 20568 (time=205.68), 0.00611863 s/step\n", + "on time step 21217 (time=212.17), 0.0061695 s/step\n", + "on time step 21864 (time=218.64), 0.00618518 s/step\n", + "on time step 22510 (time=225.1), 0.00619995 s/step\n", + "on time step 23154 (time=231.54), 0.00621246 s/step\n", + "on time step 23796 (time=237.96), 0.00623383 s/step\n", + "on time step 24448 (time=244.48), 0.00614213 s/step\n", + "field decay(t = 250.04): 7.321616676715807e-06 / 0.11846184324595802 = 6.180569604606101e-05\n", + "on time step 25105 (time=251.05), 0.00609109 s/step\n", + "on time step 25755 (time=257.55), 0.00615595 s/step\n", + "on time step 26414 (time=264.14), 0.00607499 s/step\n", + "on time step 27068 (time=270.68), 0.0061245 s/step\n", + "on time step 27695 (time=276.95), 0.00638164 s/step\n", + "on time step 28348 (time=283.48), 0.00612624 s/step\n", + "on time step 29001 (time=290.01), 0.0061314 s/step\n", + "on time step 29646 (time=296.46), 0.00621103 s/step\n", + "field decay(t = 300.04): 2.0130821207087206e-16 / 0.11846184324595802 = 1.69935066477822e-15\n", + "run 0 finished at t = 300.04 (30004 timesteps)\n", + "-----------\n", + "Initializing structure...\n", + "time for choose_chunkdivision = 5.19753e-05 s\n", + "Working in 3D dimensions.\n", + "Computational cell is 8.1 x 0.7 x 0.7 with resolution 50\n", + " block, center = (-2.025,0,0)\n", + " size (4.05,1e+20,1e+20)\n", + " axes (1,0,0), (0,1,0), (0,0,1)\n", + " dielectric constant epsilon diagonal = (2.1025,2.1025,2.1025)\n", + " cylinder, center = (0.4,0,0)\n", + " radius 0.166667, height 0.8, axis (1, 0, 0)\n", + " dielectric constant epsilon diagonal = (11.9025,11.9025,11.9025)\n", + "time for set_epsilon = 74.4824 s\n", + "-----------\n", + "on time step 495 (time=4.95), 0.00808139 s/step\n", + "on time step 1085 (time=10.85), 0.00679064 s/step\n", + "on time step 1686 (time=16.86), 0.00666365 s/step\n", + "on time step 2269 (time=22.69), 0.0068679 s/step\n", + "on time step 2863 (time=28.63), 0.0067413 s/step\n", + "on time step 3456 (time=34.56), 0.00674908 s/step\n", + "on time step 4049 (time=40.49), 0.00675245 s/step\n", + "on time step 4603 (time=46.03), 0.00722666 s/step\n", + "on time step 5203 (time=52.03), 0.00667086 s/step\n", + "on time step 5813 (time=58.13), 0.00656403 s/step\n", + "on time step 6406 (time=64.06), 0.00674696 s/step\n", + "on time step 7014 (time=70.14), 0.00658418 s/step\n", + "on time step 7615 (time=76.15), 0.00665786 s/step\n", + "on time step 8208 (time=82.08), 0.00675389 s/step\n", + "on time step 8817 (time=88.17), 0.00657699 s/step\n", + "on time step 9401 (time=94.01), 0.00685931 s/step\n", + "on time step 10007 (time=100.07), 0.00660146 s/step\n", + "on time step 10621 (time=106.21), 0.00651862 s/step\n", + "on time step 11217 (time=112.17), 0.00671972 s/step\n", + "on time step 11806 (time=118.06), 0.00679701 s/step\n", + "on time step 12397 (time=123.97), 0.00677006 s/step\n", + "on time step 12993 (time=129.93), 0.00672224 s/step\n", + "on time step 13585 (time=135.85), 0.00675993 s/step\n", + "on time step 14182 (time=141.82), 0.00670681 s/step\n", + "on time step 14791 (time=147.91), 0.0065706 s/step\n", + "on time step 15381 (time=153.81), 0.00678941 s/step\n", + "on time step 15996 (time=159.96), 0.00651369 s/step\n", + "on time step 16613 (time=166.13), 0.00648599 s/step\n", + "on time step 17221 (time=172.21), 0.0065883 s/step\n", + "on time step 17837 (time=178.37), 0.00649681 s/step\n", + "on time step 18440 (time=184.4), 0.00664288 s/step\n", + "on time step 19028 (time=190.28), 0.00680606 s/step\n", + "on time step 19634 (time=196.34), 0.00661106 s/step\n", + "on time step 20244 (time=202.44), 0.00656444 s/step\n", + "on time step 20839 (time=208.39), 0.0067259 s/step\n", + "on time step 21435 (time=214.35), 0.006716 s/step\n", + "on time step 22032 (time=220.32), 0.00670198 s/step\n", + "on time step 22614 (time=226.14), 0.00687698 s/step\n", + "on time step 23213 (time=232.13), 0.00667914 s/step\n", + "on time step 23823 (time=238.23), 0.00655979 s/step\n", + "on time step 24423 (time=244.23), 0.00667331 s/step\n", + "on time step 25033 (time=250.33), 0.00656451 s/step\n", + "on time step 25633 (time=256.33), 0.00666867 s/step\n", + "on time step 26232 (time=262.32), 0.00668129 s/step\n", + "on time step 26826 (time=268.26), 0.00673952 s/step\n", + "on time step 27427 (time=274.27), 0.00666433 s/step\n", + "on time step 28017 (time=280.17), 0.00678967 s/step\n", + "on time step 28611 (time=286.11), 0.00674255 s/step\n", + "on time step 29189 (time=291.89), 0.00692797 s/step\n", + "on time step 29775 (time=297.75), 0.00683611 s/step\n", + "on time step 30380 (time=303.8), 0.00662227 s/step\n", + "on time step 30978 (time=309.78), 0.006701 s/step\n", + "on time step 31549 (time=315.49), 0.00700918 s/step\n", + "on time step 32136 (time=321.36), 0.00681911 s/step\n", + "on time step 32732 (time=327.32), 0.00672103 s/step\n", + "on time step 33319 (time=333.19), 0.0068153 s/step\n", + "on time step 33921 (time=339.21), 0.00664554 s/step\n", + "on time step 34512 (time=345.12), 0.00677625 s/step\n", + "field decay(t = 350.05): 0.16721358207535458 / 0.16721358207535458 = 1.0\n", + "on time step 35128 (time=351.28), 0.00650192 s/step\n", + "on time step 35733 (time=357.33), 0.00662063 s/step\n", + "on time step 36343 (time=363.43), 0.00656603 s/step\n", + "on time step 36945 (time=369.45), 0.00665239 s/step\n", + "on time step 37537 (time=375.37), 0.00676581 s/step\n", + "on time step 38133 (time=381.33), 0.00672222 s/step\n", + "on time step 38735 (time=387.35), 0.00665134 s/step\n", + "on time step 39324 (time=393.24), 0.00679166 s/step\n", + "on time step 39918 (time=399.18), 0.00673842 s/step\n", + "field decay(t = 400.06): 1.1965082725381128e-16 / 0.16721358207535458 = 7.155568690579859e-16\n", + "run 0 finished at t = 400.06 (40006 timesteps)\n", + "MPB solved for frequency_1(0.625,0,0) = 0.625 after 7 iters\n", + "Dominant planewave for band 1: (0.625000,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.62963,0,0) = 0.62963 after 7 iters\n", + "Dominant planewave for band 1: (0.629630,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.634259,0,0) = 0.634259 after 7 iters\n", + "Dominant planewave for band 1: (0.634259,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.638889,0,0) = 0.638889 after 7 iters\n", + "Dominant planewave for band 1: (0.638889,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.643519,0,0) = 0.643519 after 7 iters\n", + "Dominant planewave for band 1: (0.643519,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.648148,0,0) = 0.648148 after 7 iters\n", + "Dominant planewave for band 1: (0.648148,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.652778,0,0) = 0.652778 after 7 iters\n", + "Dominant planewave for band 1: (0.652778,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.657407,0,0) = 0.657407 after 7 iters\n", + "Dominant planewave for band 1: (0.657407,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.662037,0,0) = 0.662037 after 7 iters\n", + "Dominant planewave for band 1: (0.662037,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.666667,0,0) = 0.666667 after 7 iters\n", + "Dominant planewave for band 1: (0.666667,-0.000000,0.000000)\n", + "-----------\n", + "Initializing structure...\n", + "time for choose_chunkdivision = 6.29425e-05 s\n", + "Working in 3D dimensions.\n", + "Computational cell is 8.1 x 0.7 x 0.7 with resolution 50\n", + "time for set_epsilon = 0.791664 s\n", + "-----------\n", + "on time step 519 (time=5.19), 0.00771539 s/step\n", + "on time step 1175 (time=11.75), 0.00610057 s/step\n", + "on time step 1828 (time=18.28), 0.00613267 s/step\n", + "on time step 2472 (time=24.72), 0.00622066 s/step\n", + "on time step 3117 (time=31.17), 0.0062042 s/step\n", + "on time step 3762 (time=37.62), 0.0062109 s/step\n", + "on time step 4412 (time=44.12), 0.00615934 s/step\n", + "field decay(t = 50.01): 5.426741164771886e-06 / 5.426741164771886e-06 = 1.0\n", + "on time step 5055 (time=50.55), 0.00622507 s/step\n", + "on time step 5707 (time=57.07), 0.00613604 s/step\n", + "on time step 6353 (time=63.53), 0.00619805 s/step\n", + "on time step 6993 (time=69.93), 0.00625692 s/step\n", + "on time step 7630 (time=76.3), 0.00628907 s/step\n", + "on time step 8283 (time=82.83), 0.00612956 s/step\n", + "on time step 8917 (time=89.17), 0.00630966 s/step\n", + "on time step 9499 (time=94.99), 0.00688505 s/step\n", + "field decay(t = 100.01): 0.035570203188044235 / 0.035570203188044235 = 1.0\n", + "on time step 10101 (time=101.01), 0.00665091 s/step\n", + "on time step 10719 (time=107.19), 0.00647517 s/step\n", + "on time step 11343 (time=113.43), 0.00641158 s/step\n", + "on time step 11982 (time=119.82), 0.00626142 s/step\n", + "on time step 12629 (time=126.29), 0.00618784 s/step\n", + "on time step 13272 (time=132.72), 0.00622975 s/step\n", + "on time step 13923 (time=139.23), 0.00614737 s/step\n", + "on time step 14576 (time=145.76), 0.00612626 s/step\n", + "field decay(t = 150.02): 0.11846184324595802 / 0.11846184324595802 = 1.0\n", + "on time step 15213 (time=152.13), 0.00628418 s/step\n", + "on time step 15865 (time=158.65), 0.00614363 s/step\n", + "on time step 16526 (time=165.26), 0.00605562 s/step\n", + "on time step 17165 (time=171.65), 0.00626528 s/step\n", + "on time step 17814 (time=178.14), 0.00616657 s/step\n", + "on time step 18477 (time=184.77), 0.00604035 s/step\n", + "on time step 19110 (time=191.1), 0.00632727 s/step\n", + "on time step 19765 (time=197.65), 0.00611466 s/step\n", + "field decay(t = 200.03): 0.040856206457377704 / 0.11846184324595802 = 0.3448891671603442\n", + "on time step 20412 (time=204.12), 0.00618811 s/step\n", + "on time step 21068 (time=210.68), 0.00610492 s/step\n", + "on time step 21715 (time=217.15), 0.00618881 s/step\n", + "on time step 22375 (time=223.75), 0.00606571 s/step\n", + "on time step 23043 (time=230.43), 0.00599698 s/step\n", + "on time step 23711 (time=237.11), 0.00599475 s/step\n", + "on time step 24375 (time=243.75), 0.00602797 s/step\n", + "field decay(t = 250.04): 7.321616676715807e-06 / 0.11846184324595802 = 6.180569604606101e-05\n", + "on time step 25039 (time=250.39), 0.00602749 s/step\n", + "on time step 25687 (time=256.87), 0.00617486 s/step\n", + "on time step 26331 (time=263.31), 0.00621907 s/step\n", + "on time step 26984 (time=269.84), 0.00612766 s/step\n", + "on time step 27631 (time=276.31), 0.00618452 s/step\n", + "on time step 28280 (time=282.8), 0.00616904 s/step\n", + "on time step 28910 (time=289.1), 0.00635051 s/step\n", + "on time step 29547 (time=295.47), 0.00628384 s/step\n", + "field decay(t = 300.04): 2.0130821207087206e-16 / 0.11846184324595802 = 1.69935066477822e-15\n", + "run 0 finished at t = 300.04 (30004 timesteps)\n", + "-----------\n", + "Initializing structure...\n", + "time for choose_chunkdivision = 5.60284e-05 s\n", + "Working in 3D dimensions.\n", + "Computational cell is 8.1 x 0.7 x 0.7 with resolution 50\n", + " block, center = (-2.025,0,0)\n", + " size (4.05,1e+20,1e+20)\n", + " axes (1,0,0), (0,1,0), (0,0,1)\n", + " dielectric constant epsilon diagonal = (2.1025,2.1025,2.1025)\n", + " cylinder, center = (0.4,0,0)\n", + " radius 0.233333, height 0.8, axis (1, 0, 0)\n", + " dielectric constant epsilon diagonal = (11.9025,11.9025,11.9025)\n", + "time for set_epsilon = 111.664 s\n", + "-----------\n", + "on time step 490 (time=4.9), 0.00816926 s/step\n", + "on time step 1087 (time=10.87), 0.0067107 s/step\n", + "on time step 1679 (time=16.79), 0.00676114 s/step\n", + "on time step 2283 (time=22.83), 0.00662823 s/step\n", + "on time step 2888 (time=28.88), 0.00661911 s/step\n", + "on time step 3488 (time=34.88), 0.00667222 s/step\n", + "on time step 4072 (time=40.72), 0.00685681 s/step\n", + "on time step 4666 (time=46.66), 0.0067412 s/step\n", + "on time step 5265 (time=52.65), 0.00668597 s/step\n", + "on time step 5863 (time=58.63), 0.00669887 s/step\n", + "on time step 6449 (time=64.49), 0.00682794 s/step\n", + "on time step 7040 (time=70.4), 0.0067765 s/step\n", + "on time step 7652 (time=76.52), 0.00654109 s/step\n", + "on time step 8263 (time=82.63), 0.0065542 s/step\n", + "on time step 8855 (time=88.55), 0.00676295 s/step\n", + "on time step 9407 (time=94.07), 0.00724843 s/step\n", + "on time step 10009 (time=100.09), 0.00665044 s/step\n", + "on time step 10616 (time=106.16), 0.0065973 s/step\n", + "on time step 11216 (time=112.16), 0.00667265 s/step\n", + "on time step 11812 (time=118.12), 0.00671279 s/step\n", + "on time step 12407 (time=124.07), 0.00672697 s/step\n", + "on time step 13009 (time=130.09), 0.00665021 s/step\n", + "on time step 13601 (time=136.01), 0.00676467 s/step\n", + "on time step 14200 (time=142), 0.00667941 s/step\n", + "on time step 14799 (time=147.99), 0.0066795 s/step\n", + "on time step 15388 (time=153.88), 0.00679397 s/step\n", + "on time step 15967 (time=159.67), 0.0069204 s/step\n", + "on time step 16516 (time=165.16), 0.00728701 s/step\n", + "on time step 17115 (time=171.15), 0.0066783 s/step\n", + "on time step 17730 (time=177.3), 0.00650957 s/step\n", + "on time step 18302 (time=183.02), 0.00700086 s/step\n", + "on time step 18911 (time=189.11), 0.00657568 s/step\n", + "on time step 19522 (time=195.22), 0.00655391 s/step\n", + "on time step 20114 (time=201.14), 0.00676642 s/step\n", + "on time step 20725 (time=207.25), 0.00655666 s/step\n", + "on time step 21325 (time=213.25), 0.00667417 s/step\n", + "on time step 21939 (time=219.39), 0.00652287 s/step\n", + "on time step 22559 (time=225.59), 0.00645762 s/step\n", + "on time step 23158 (time=231.58), 0.00668563 s/step\n", + "on time step 23767 (time=237.67), 0.00657393 s/step\n", + "on time step 24384 (time=243.84), 0.00649162 s/step\n", + "on time step 24986 (time=249.86), 0.00664588 s/step\n", + "on time step 25596 (time=255.96), 0.00656288 s/step\n", + "on time step 26216 (time=262.16), 0.00645253 s/step\n", + "on time step 26822 (time=268.22), 0.00660776 s/step\n", + "on time step 27421 (time=274.21), 0.00667996 s/step\n", + "on time step 28018 (time=280.18), 0.00671012 s/step\n", + "on time step 28630 (time=286.3), 0.0065457 s/step\n", + "on time step 29225 (time=292.25), 0.00673055 s/step\n", + "on time step 29821 (time=298.21), 0.00671857 s/step\n", + "on time step 30441 (time=304.41), 0.00645416 s/step\n", + "on time step 31052 (time=310.52), 0.00655298 s/step\n", + "on time step 31656 (time=316.56), 0.00662306 s/step\n", + "on time step 32257 (time=322.57), 0.00666524 s/step\n", + "on time step 32855 (time=328.55), 0.00669196 s/step\n", + "on time step 33447 (time=334.47), 0.00675835 s/step\n", + "on time step 34054 (time=340.54), 0.0066009 s/step\n", + "on time step 34653 (time=346.53), 0.00668701 s/step\n", + "field decay(t = 350.05): 0.14533155250113108 / 0.14533155250113108 = 1.0\n", + "on time step 35265 (time=352.65), 0.00653655 s/step\n", + "on time step 35855 (time=358.55), 0.00679268 s/step\n", + "on time step 36422 (time=364.22), 0.00706286 s/step\n", + "on time step 37016 (time=370.16), 0.00674342 s/step\n", + "on time step 37625 (time=376.25), 0.00657667 s/step\n", + "on time step 38207 (time=382.07), 0.00688101 s/step\n", + "on time step 38793 (time=387.93), 0.00683809 s/step\n", + "on time step 39387 (time=393.87), 0.00673525 s/step\n", + "on time step 39986 (time=399.86), 0.0066873 s/step\n", + "field decay(t = 400.06): 1.740064432387871e-11 / 0.14533155250113108 = 1.1973067117509314e-10\n", + "run 0 finished at t = 400.06 (40006 timesteps)\n", + "MPB solved for frequency_1(0.625,0,0) = 0.625 after 7 iters\n", + "Dominant planewave for band 1: (0.625000,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.62963,0,0) = 0.62963 after 7 iters\n", + "Dominant planewave for band 1: (0.629630,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.634259,0,0) = 0.634259 after 7 iters\n", + "Dominant planewave for band 1: (0.634259,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.638889,0,0) = 0.638889 after 7 iters\n", + "Dominant planewave for band 1: (0.638889,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.643519,0,0) = 0.643519 after 7 iters\n", + "Dominant planewave for band 1: (0.643519,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.648148,0,0) = 0.648148 after 7 iters\n", + "Dominant planewave for band 1: (0.648148,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.652778,0,0) = 0.652778 after 7 iters\n", + "Dominant planewave for band 1: (0.652778,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.657407,0,0) = 0.657407 after 7 iters\n", + "Dominant planewave for band 1: (0.657407,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.662037,0,0) = 0.662037 after 7 iters\n", + "Dominant planewave for band 1: (0.662037,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.666667,0,0) = 0.666667 after 7 iters\n", + "Dominant planewave for band 1: (0.666667,-0.000000,0.000000)\n", + "-----------\n", + "Initializing structure...\n", + "time for choose_chunkdivision = 6.69956e-05 s\n", + "Working in 3D dimensions.\n", + "Computational cell is 8.1 x 0.7 x 0.7 with resolution 50\n", + "time for set_epsilon = 0.80387 s\n", + "-----------\n", + "on time step 548 (time=5.48), 0.00730527 s/step\n", + "on time step 1190 (time=11.9), 0.00623872 s/step\n", + "on time step 1845 (time=18.45), 0.00611096 s/step\n", + "on time step 2527 (time=25.27), 0.00586759 s/step\n", + "on time step 3196 (time=31.96), 0.0059849 s/step\n", + "on time step 3831 (time=38.31), 0.00630408 s/step\n", + "on time step 4484 (time=44.84), 0.00613357 s/step\n", + "field decay(t = 50.01): 5.426741164771886e-06 / 5.426741164771886e-06 = 1.0\n", + "on time step 5114 (time=51.14), 0.00635817 s/step\n", + "on time step 5753 (time=57.53), 0.00626071 s/step\n", + "on time step 6405 (time=64.05), 0.00613555 s/step\n", + "on time step 7052 (time=70.52), 0.00618312 s/step\n", + "on time step 7712 (time=77.12), 0.00606276 s/step\n", + "on time step 8384 (time=83.84), 0.00595316 s/step\n", + "on time step 9029 (time=90.29), 0.00620189 s/step\n", + "on time step 9689 (time=96.89), 0.00607118 s/step\n", + "field decay(t = 100.01): 0.035570203188044235 / 0.035570203188044235 = 1.0\n", + "on time step 10344 (time=103.44), 0.00610808 s/step\n", + "on time step 10989 (time=109.89), 0.00620215 s/step\n", + "on time step 11631 (time=116.31), 0.00623073 s/step\n", + "on time step 12269 (time=122.69), 0.00627587 s/step\n", + "on time step 12903 (time=129.03), 0.00631806 s/step\n", + "on time step 13546 (time=135.46), 0.006229 s/step\n", + "on time step 14179 (time=141.79), 0.00632034 s/step\n", + "on time step 14785 (time=147.85), 0.00661135 s/step\n", + "field decay(t = 150.02): 0.11846184324595802 / 0.11846184324595802 = 1.0\n", + "on time step 15428 (time=154.28), 0.00623013 s/step\n", + "on time step 16052 (time=160.52), 0.00641477 s/step\n", + "on time step 16686 (time=166.86), 0.00630934 s/step\n", + "on time step 17331 (time=173.31), 0.00620647 s/step\n", + "on time step 17959 (time=179.59), 0.00637627 s/step\n", + "on time step 18610 (time=186.1), 0.006145 s/step\n", + "on time step 19261 (time=192.61), 0.00614484 s/step\n", + "on time step 19922 (time=199.22), 0.00605228 s/step\n", + "field decay(t = 200.03): 0.040856206457377704 / 0.11846184324595802 = 0.3448891671603442\n", + "on time step 20552 (time=205.52), 0.0063599 s/step\n", + "on time step 21175 (time=211.75), 0.00642603 s/step\n", + "on time step 21800 (time=218), 0.00640625 s/step\n", + "on time step 22450 (time=224.5), 0.00616006 s/step\n", + "on time step 23091 (time=230.91), 0.00624342 s/step\n", + "on time step 23727 (time=237.27), 0.00629158 s/step\n", + "on time step 24336 (time=243.36), 0.00657378 s/step\n", + "on time step 24976 (time=249.76), 0.0062545 s/step\n", + "field decay(t = 250.04): 7.321616676715807e-06 / 0.11846184324595802 = 6.180569604606101e-05\n", + "on time step 25621 (time=256.21), 0.0062098 s/step\n", + "on time step 26264 (time=262.64), 0.00622471 s/step\n", + "on time step 26909 (time=269.09), 0.00620622 s/step\n", + "on time step 27541 (time=275.41), 0.00633158 s/step\n", + "on time step 28185 (time=281.85), 0.00621162 s/step\n", + "on time step 28816 (time=288.16), 0.00634153 s/step\n", + "on time step 29475 (time=294.75), 0.00607585 s/step\n", + "field decay(t = 300.04): 2.0130821207087206e-16 / 0.11846184324595802 = 1.69935066477822e-15\n", + "run 0 finished at t = 300.04 (30004 timesteps)\n", + "-----------\n", + "Initializing structure...\n", + "time for choose_chunkdivision = 8.4877e-05 s\n", + "Working in 3D dimensions.\n", + "Computational cell is 8.1 x 0.7 x 0.7 with resolution 50\n", + " block, center = (-2.025,0,0)\n", + " size (4.05,1e+20,1e+20)\n", + " axes (1,0,0), (0,1,0), (0,0,1)\n", + " dielectric constant epsilon diagonal = (2.1025,2.1025,2.1025)\n", + " cylinder, center = (0.4,0,0)\n", + " radius 0.3, height 0.8, axis (1, 0, 0)\n", + " dielectric constant epsilon diagonal = (11.9025,11.9025,11.9025)\n", + "time for set_epsilon = 131.917 s\n", + "-----------\n", + "on time step 516 (time=5.16), 0.00775214 s/step\n", + "on time step 1132 (time=11.32), 0.00649492 s/step\n", + "on time step 1710 (time=17.1), 0.00693608 s/step\n", + "on time step 2305 (time=23.05), 0.00673325 s/step\n", + "on time step 2908 (time=29.08), 0.0066383 s/step\n", + "on time step 3512 (time=35.12), 0.00662362 s/step\n", + "on time step 4116 (time=41.16), 0.0066338 s/step\n", + "on time step 4734 (time=47.34), 0.00648148 s/step\n", + "on time step 5345 (time=53.45), 0.00654778 s/step\n", + "on time step 5953 (time=59.53), 0.00658071 s/step\n", + "on time step 6545 (time=65.45), 0.00675819 s/step\n", + "on time step 7166 (time=71.66), 0.00644515 s/step\n", + "on time step 7786 (time=77.86), 0.0064575 s/step\n", + "on time step 8407 (time=84.07), 0.00645203 s/step\n", + "on time step 9021 (time=90.21), 0.00651867 s/step\n", + "on time step 9626 (time=96.26), 0.00661657 s/step\n", + "on time step 10248 (time=102.48), 0.00643916 s/step\n", + "on time step 10825 (time=108.25), 0.00694631 s/step\n", + "on time step 11406 (time=114.06), 0.00689218 s/step\n", + "on time step 11998 (time=119.98), 0.00676169 s/step\n", + "on time step 12612 (time=126.12), 0.00651705 s/step\n", + "on time step 13216 (time=132.16), 0.0066305 s/step\n", + "on time step 13826 (time=138.26), 0.00656524 s/step\n", + "on time step 14437 (time=144.37), 0.00655429 s/step\n", + "on time step 15046 (time=150.46), 0.00657743 s/step\n", + "on time step 15656 (time=156.56), 0.00656639 s/step\n", + "on time step 16256 (time=162.56), 0.00667171 s/step\n", + "on time step 16870 (time=168.7), 0.00652351 s/step\n", + "on time step 17489 (time=174.89), 0.00647183 s/step\n", + "on time step 18084 (time=180.84), 0.00672594 s/step\n", + "on time step 18679 (time=186.79), 0.00672316 s/step\n", + "on time step 19285 (time=192.85), 0.00660615 s/step\n", + "on time step 19885 (time=198.85), 0.00667117 s/step\n", + "on time step 20489 (time=204.89), 0.00662797 s/step\n", + "on time step 21104 (time=211.04), 0.0065097 s/step\n", + "on time step 21710 (time=217.1), 0.00660689 s/step\n", + "on time step 22329 (time=223.29), 0.00646226 s/step\n", + "on time step 22942 (time=229.42), 0.00652912 s/step\n", + "on time step 23555 (time=235.55), 0.00653024 s/step\n", + "on time step 24171 (time=241.71), 0.00649752 s/step\n", + "on time step 24778 (time=247.78), 0.00659123 s/step\n", + "on time step 25390 (time=253.9), 0.00654119 s/step\n", + "on time step 25987 (time=259.87), 0.00670381 s/step\n", + "on time step 26583 (time=265.83), 0.0067193 s/step\n", + "on time step 27197 (time=271.97), 0.00652167 s/step\n", + "on time step 27803 (time=278.03), 0.00660775 s/step\n", + "on time step 28413 (time=284.13), 0.00656554 s/step\n", + "on time step 29013 (time=290.13), 0.00666845 s/step\n", + "on time step 29604 (time=296.04), 0.00677457 s/step\n", + "on time step 30200 (time=302), 0.00671849 s/step\n", + "on time step 30811 (time=308.11), 0.00655369 s/step\n", + "on time step 31404 (time=314.04), 0.00675066 s/step\n", + "on time step 32019 (time=320.19), 0.0065148 s/step\n", + "on time step 32638 (time=326.38), 0.00646644 s/step\n", + "on time step 33249 (time=332.49), 0.00655209 s/step\n", + "on time step 33854 (time=338.54), 0.00661389 s/step\n", + "on time step 34452 (time=344.52), 0.00669173 s/step\n", + "field decay(t = 350.05): 0.16032430386920205 / 0.16032430386920205 = 1.0\n", + "on time step 35049 (time=350.49), 0.00670484 s/step\n", + "on time step 35645 (time=356.45), 0.00672067 s/step\n", + "on time step 36257 (time=362.57), 0.00654534 s/step\n", + "on time step 36872 (time=368.72), 0.00650921 s/step\n", + "on time step 37484 (time=374.84), 0.00654621 s/step\n", + "on time step 38065 (time=380.65), 0.00688614 s/step\n", + "on time step 38660 (time=386.6), 0.00672478 s/step\n", + "on time step 39279 (time=392.79), 0.0064637 s/step\n", + "on time step 39892 (time=398.92), 0.00653673 s/step\n", + "field decay(t = 400.06): 2.282200281523022e-12 / 0.16032430386920205 = 1.4234899054262652e-11\n", + "run 0 finished at t = 400.06 (40006 timesteps)\n", + "MPB solved for frequency_1(0.625,0,0) = 0.625 after 7 iters\n", + "Dominant planewave for band 1: (0.625000,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.62963,0,0) = 0.62963 after 7 iters\n", + "Dominant planewave for band 1: (0.629630,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.634259,0,0) = 0.634259 after 7 iters\n", + "Dominant planewave for band 1: (0.634259,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.638889,0,0) = 0.638889 after 7 iters\n", + "Dominant planewave for band 1: (0.638889,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.643519,0,0) = 0.643519 after 7 iters\n", + "Dominant planewave for band 1: (0.643519,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.648148,0,0) = 0.648148 after 7 iters\n", + "Dominant planewave for band 1: (0.648148,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.652778,0,0) = 0.652778 after 7 iters\n", + "Dominant planewave for band 1: (0.652778,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.657407,0,0) = 0.657407 after 7 iters\n", + "Dominant planewave for band 1: (0.657407,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.662037,0,0) = 0.662037 after 7 iters\n", + "Dominant planewave for band 1: (0.662037,-0.000000,0.000000)\n", + "MPB solved for frequency_1(0.666667,0,0) = 0.666667 after 7 iters\n", + "Dominant planewave for band 1: (0.666667,-0.000000,0.000000)\n" + ] + } + ], + "source": [ + "P = 0.7 #unit cell pitch\n", + "h = 0.8 #pillar height\n", + "\n", + "def unitCell_paramsweep(D):\n", + " sim_x_size = dpml + dsub + dpad + dpml\n", + " sim_z_size = P\n", + " sim_y_size = P\n", + "\n", + " cell_size = mp.Vector3(sim_x_size, sim_y_size, sim_z_size)\n", + "\n", + " source_position = mp.Vector3(-wvl_cen,0,0) #source position in the middle of the substrate\n", + " source = mp.Source(\n", + " mp.GaussianSource(fcen, fwidth=df),\n", + " component = mp.Ez,\n", + " center = source_position,\n", + " size = mp.Vector3(0, sim_y_size, sim_z_size),\n", + " )\n", + "\n", + " pml_layers = [mp.PML(dpml,direction=mp.X)]\n", + "\n", + " monitor_position = mp.Vector3(h + wvl_cen,0,0)\n", + " obs_point = monitor_position\n", + " stop_cond = mp.stop_when_fields_decayed(50, mp.Ez, obs_point, 1e-9)\n", + " #stop_cond_time = 50\n", + " sim = mp.Simulation(\n", + " resolution = resolution,\n", + " cell_size = cell_size,\n", + " boundary_layers = pml_layers,\n", + " k_point = k_point,\n", + " default_material = sio2,\n", + " sources = [source],\n", + " )\n", + "\n", + " flux_mon = sim.add_flux(\n", + " fcen, \n", + " df,\n", + " nfreq, \n", + " mp.FluxRegion(center=monitor_position, size=mp.Vector3(0, sim_y_size, sim_z_size))\n", + " )\n", + "\n", + " sim.run(until_after_sources=stop_cond)\n", + " input_flux = mp.get_fluxes(flux_mon)\n", + "\n", + " sim.reset_meep()\n", + "\n", + " pillar_geo = mp.Cylinder(\n", + " material = si,\n", + " radius = D/2,\n", + " height = h,\n", + " center = mp.Vector3(h/2,0,0),\n", + " axis=mp.Vector3(1,0,0),\n", + " )\n", + "\n", + " substrate_geo = mp.Block(\n", + " material = sio2,\n", + " size = mp.Vector3(dsub+dpml, mp.inf, mp.inf),\n", + " center = mp.Vector3(-(dsub+dpml)/2,0,0),\n", + " )\n", + "\n", + " geometries = [substrate_geo, pillar_geo]\n", + "\n", + " sim = mp.Simulation(\n", + " resolution = resolution,\n", + " cell_size = cell_size,\n", + " boundary_layers = pml_layers,\n", + " geometry = geometries,\n", + " k_point = k_point,\n", + " sources = [source],\n", + " )\n", + "\n", + " mode_mon = sim.add_flux(\n", + " fcen, df, nfreq, mp.FluxRegion(center=monitor_position, size=mp.Vector3(0, sim_y_size, sim_z_size))\n", + " )#this monitor accumulates fields \n", + "\n", + " f, ax = plt.subplots(1,2,dpi=120)\n", + " xy_plane = mp.Volume(center=mp.Vector3(0,0,h/2), size=mp.Vector3(sim_x_size,sim_y_size,0))\n", + " sim.plot2D(ax=ax[1],output_plane=xy_plane,show_sources=True,show_monitors=True,show_epsilon=True)\n", + " ax[1].set_xlim(-sim_x_size/2, sim_x_size/2)\n", + " ax[1].set_ylim(-sim_y_size/2, sim_y_size/2)\n", + " plt.show()\n", + "\n", + " sim.run(until_after_sources=stop_cond)\n", + "\n", + " freqs = mp.get_eigenmode_freqs(mode_mon)\n", + " res = sim.get_eigenmode_coefficients(\n", + " mode_mon, [1], eig_parity=mp.ODD_Z + mp.EVEN_Y\n", + " )\n", + " coeffs = res.alpha\n", + "\n", + " mode_wvl = [1/freqs[nf] for nf in range(nfreq)]\n", + " mode_tran = [abs(coeffs[0,nf,0])**2 / input_flux[nf] for nf in range(nfreq)]\n", + " mode_phase = [np.angle(coeffs[0,nf,0]) for nf in range(nfreq)]\n", + "\n", + " return mode_wvl, mode_tran, mode_phase\n", + "\n", + "diameters = np.linspace(0.2, 0.6, 4)\n", + "mode_tran_arr = np.empty((len(diameters), nfreq))\n", + "mode_phase_arr = np.empty((len(diameters), nfreq))\n", + "\n", + "for i, D in enumerate(diameters):\n", + " mode_wvl, mode_tran, mode_phase = unitCell_paramsweep(D)\n", + " mode_tran_arr[i,:] = mode_tran\n", + " mode_phase_arr[i,:] = mode_phase" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "93cee842", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAloAAAHGCAYAAACsFyVPAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsfQe4LEXRdu/uuZecBEVEjKigKEZQUFCCiIqIAQRFQRADYlbE8AsmDJ8REyZUVMCAihlFFAMifuinKKiIShBRQSTDPWf3f6q6q7qqunt2ZnfPPefsmbrP3J2dnenp6TPT8/Zb1W91BoPBwLXWWmuttdZaa621NnHrTr7I1lprrbXWWmuttdZaoNVaa6211lprrbU2j9YyWq211lprrbXWWmvzZC3Qaq211lprrbXWWpsna4FWa6211lprrbXW2jxZC7Raa6211lprrbXW5slaoNVaa6211lprrbU2T9YCrdZaa6211lprrbV5shZotbba7b///a9773vf637/+98vqbJba621xWnwvMNz/5///Gehq9Jaa4nNpJtaWx32xz/+0X3rW9+qte/BBx/sNthgAzct9q9//cu99KUvdSeccIK7973vvWTKbq211han/eIXv8Dn/jGPeYzbaKONFro6rbWmrAVaC2Q33HCD++tf/6q2fehDH3K3v/3t3ZOe9CS1fdWqVW6abMMNN3QvfvGL5wUIzWfZrbXWWmuttdbUOm2uw8Vj6667rnvoQx/qvv/97y90VVprrbXWlox96lOfQub/ggsucFtttdVCV6e11pS1jNYitn/+85/u85//vHv84x/v7nKXu7jTTz/dXXzxxch43fa2t3Uf/OAHed811ljD3elOd3K77LKLW2uttbJl3PWud3Xf+c533KWXXuruc5/7uEc84hHJOYE9+8EPfuAuueQSpOC32247LDdX3p3vfGf37W9/G7fttNNO7p73vCeXAee5/PLL3fbbb+8e8IAHJHFU4Np79KMfrZinYeeus0+pbLCrrroKjwX34h3ucAdsq/XXX3/ktmqttdbmN+4K+rxnPetZrtfruW9+85vuuuuucw9/+MMrGesbb7zRfeMb38B4rR122MHd9773Vb//4x//cCeffDKudzodt+aaa7p73ete+IzDeaTB+c4880x3xRVXYJ/xsIc9zG2yySbJOeFcsB+Ufbvb3c7tvvvuUxXu0dp41gbDL2IDMAFxB2eccYbbddddEUCcdNJJCLYGgwG6Hmk555xz3Ite9CIEZL/61a+SMn70ox9h/MJnPvMZ973vfc898pGPdIcccog635/+9Cd3t7vdDV1vUN6pp57qdtttN/f//t//S8oDwAJg5pRTTnFf+MIXEIx88YtfdFdeeSUCGKjn1772NfegBz1IAUIZRwVxFU3OXWefXNlg0HYAyN72tre5X/7yl+61r30tthV03qO0VWuttbZ64q4AwMAzCIM66GO22WYbd9RRR2WP+fOf/4x9JQAt2Hfbbbd17373u9U+t9xyC/ebf/nLX9xZZ53lnvrUp2IfBoMtMuh373jHO7q3vvWt2KdCHwIDR+gXpH3sYx/DvuXYY4915513nvuf//kfHKh997vfnaeWaW3JGbgOW1scts466wx23XVX/n7uuecO4E90j3vcY3DBBRfgtrm5ucGVV16ZPX52dnaw++67Dx7wgAckZdz73vce/PGPf+Tt73nPe3D7L3/5S9524IEHDrbccsvBrbfeqso8/fTTs+VdfPHF6thNN910cMABBwwuuugi3n7wwQcPNtpoo8F1113H2/70pz9hGSeccEKjc9fZJ1c21LnX6w0OO+ywQb/fx21Qxt577z1Ye+21B3/5y18at1VrrbU2vwbPMDx3O+200+Bf//oXb3/f+96H20899dRk3912223w73//m7cfcsgh2K/KbTm75pprsG+BPoYM+tHHPvaxar/rr79+cNZZZ/H373znO4NOpzM45phj1H4vfOELB+utt97gH//4x4hX39o0WctoLQEDGpriDrrdLlLTZP/3f//nPv3pT7v3v//97rjjjsM4r1//+tdIn0vbc8893T3ucQ/+/sxnPhM/f/jDHyr6u9/vu1tvvZW3AZUO57f22Mc+FkdtZPvttx+yWVC3u9/97rx93333xXKhTlVW59xN6iftox/9KLYbjEzBVQC2YsUK9/a3vx3bCeI7mrZVa621tnrsgAMOUO66F77whcggfeQjH0n2ffrTn+423nhj9ezCxCNgsaVBP/LTn/7UfeITn3Dve9/7kK2Cc8A22d9AqMLc3BxvW2eddVQYwTvf+U50KQJDLu2YY45BtyO5KFtb3tbGaC0Bu9/97pdsu/baa90Tn/hEpNfBVQcU98qVKzFGCdyK4EKDGCoyoNul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dZeFn80TTzyRmw/biNg3BM64zzzzDPl3afFvt4EtNtZbb70Rl0Wpewy0H9Hnp5FlugUwbv2///s/OncLLLBAQ+vATwkN0RdddNFSXyU0KoZtAlrYNFPokTB5ASE9bCI+9KEPkUC+Hh566CFpw8NYYoklpEuGBsYzjIFrrrlm14rnYZeBc7LiiitKYUIZ4GEG66AY6DXZzDLdAki10S8Y37l11lmn4fUw/sDUG9+5orEN49jDDz9MPlqrr7666VlkXY7nn38+O/LII7Mll1ySLB/OOeecEde55JJLsmnTpmWbbbZZMM2ePTtYDqXY88wzT7bGGmvQI6woqtVq1k341re+la266qrZoosuKjYb9cCl7muttVZw7n796183tUw3AOX8u+66a7b44ovT5/3rX//a0HqXXnpptthii2Urr7xytvrqq2e77bZbUMIP+4QddtghW2CBBehvg0d8ZxM6F9dff3221VZbZe9973vpu/Lqq6+OuA5+M8suu2zwGzr99NODZV566aVso402ot8v7DWwfVjjdBOee+657LOf/SyN8VOmTKExayR873vfy+add97cGD88PNzUMp2OWq2Wff3rX6exBt+RmTNnNrTeO++8k+23337Z/PPPT+vge/iDH/wgWOaWW27J3ve+92UrrLACjWdrr702/a16EV1PtOCpdcEFF9CFaumll26YaGHZegApAHG788476fVjjz1GPjjf/va3s27CMccckz366KPZiSee2BTRuvXWW9taphuAgRo+bg888EDDROvaa6/N+vv7s6uvvlrm3XDDDdkzzzwjrw8//HAaGP/973/L9xXr4O+U0Jk499xzs5tvvpn+1s0QrZhYxQDR33jjjeUm8ayzziIPs1deeSXrFvzhD3+g3xpuQHBBb5RowVux3WU6HQMDA9mxxx6bPf744zTWN0q09thjDwowvPjii/R67ty52Y9+9CN5H9db/C1OOOEE8cDEjcTmm2+e9SK6nmhpNEO0llpqqeyhhx7KHn74Yfoyxpg1a1a24447BvMOPfTQbJ111sm6Ec0SLZgw3nfffUIGml2mm4DvUaNEa7311iOjyzJgwMKF8rzzzgvmw/gSf6OEzsaNN97YFNH64he/mN1zzz0UuY8BE+dKpZJdc801Mg+ECxFQNlftNjRDtFZcccXswQcfzB555JHCMb6RZboJjRKt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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure()\n", + "plt.subplot(1,2,1)\n", + "plt.pcolormesh(\n", + " mode_wvl,\n", + " diameters,\n", + " mode_tran_arr,\n", + " cmap='hot_r',\n", + " shading='gouraud',\n", + " vmin=0,\n", + " vmax=mode_tran_arr.max(),\n", + ")\n", + "\n", + "plt.axis([wvl_min, wvl_max, diameters[0], diameters[-1]])\n", + "plt.xlabel(\"Wavelength (um)\")\n", + "plt.xticks([t for t in np.linspace(wvl_min, wvl_max, 3)])\n", + "plt.ylabel(\"Pillar Diameter (um)\")\n", + "plt.title(\"Transmission\")\n", + "cbar=plt.colorbar()\n", + "cbar.set_ticks([t for t in np.arange(0, 1.2, 0.2)])\n", + "cbar.set_ticklabels([\"{:.1f}\".format(t) for t in np.linspace(0, 1, 6)])\n", + "\n", + "plt.subplot(1,2,2)\n", + "plt.pcolormesh(\n", + " mode_wvl,\n", + " diameters,\n", + " mode_phase_arr,\n", + " cmap='RdBu',\n", + " shading='gouraud',\n", + " vmin=mode_phase_arr.min(),\n", + " vmax=mode_phase_arr.max(),\n", + ")\n", + "\n", + "plt.axis([wvl_min, wvl_max, diameters[0], diameters[-1]])\n", + "plt.xlabel(\"Wavelength (um)\")\n", + "plt.xticks([t for t in np.linspace(wvl_min, wvl_max, 3)])\n", + "plt.ylabel(\"Pillar Diameter (um)\")\n", + "plt.title(\"phase\")\n", + "cbar=plt.colorbar()\n", + "cbar.set_ticks([t for t in range(-3, 4)])\n", + "cbar.set_ticklabels([\"{:.1f}\".format(t) for t in range(-3,4)])\n", + "\n", + "plt.subplots_adjust(wspace=0.5)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "mp", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/unit_cell/unit_cell_param_sweep.pdf b/unit_cell/unit_cell_param_sweep.pdf new file mode 100644 index 0000000..426c899 Binary files /dev/null and b/unit_cell/unit_cell_param_sweep.pdf differ diff --git a/unit_cell/unit_cell_param_sweep.py b/unit_cell/unit_cell_param_sweep.py new file mode 100644 index 0000000..8873808 --- /dev/null +++ b/unit_cell/unit_cell_param_sweep.py @@ -0,0 +1,203 @@ +import meep as mp +mp.verbosity(0) + +import numpy as np +import matplotlib.pyplot as plt +from mpi4py import MPI + +comm = MPI.COMM_WORLD +rank = comm.Get_rank() +size = comm.Get_size() + +resolution = 50 # pixels/unit length + +wvl_min = 1.5 +wvl_max = 1.6 +wvl_cen = 0.5*(wvl_min+wvl_max) +fmin = 1/wvl_max +fmax = 1/wvl_min +fcen = 1/wvl_cen +df = fmax-fmin +nfreq = 20 + +dpml = 1.0 #PML thickness +dsub = 1.5 + wvl_cen #substrate thickness +dpad = dsub #padding between cell and PML + +k_point = mp.Vector3(0,0,0) #used for PBC + +n_si = 3.45 #silicon refractive index +si = mp.Medium(index=n_si) +n_sio2 = 1.45 +sio2 = mp.Medium(index=n_sio2) +n_air = 1.0 #air refractive index +air = mp.Medium(index=n_air) + +P = 0.7 #unit cell pitch +h = 0.9 #pillar height + +def unitCell_paramsweep(D): + print("Rank {}: Running simulation for diameter {}".format(rank, D)) + sim_x_size = dpml + dsub + dpad + dpml + sim_z_size = P + sim_y_size = P + + cell_size = mp.Vector3(sim_x_size, sim_y_size, sim_z_size) + + source_position = mp.Vector3(-wvl_cen,0,0) #source position in the middle of the substrate + source = mp.Source( + mp.GaussianSource(fcen, fwidth=df), + component = mp.Ez, + center = source_position, + size = mp.Vector3(0, sim_y_size, sim_z_size), + ) + + pml_layers = [mp.PML(dpml,direction=mp.X)] + + monitor_position = mp.Vector3(h + wvl_cen,0,0) + obs_point = monitor_position + stop_cond = mp.stop_when_fields_decayed(50, mp.Ez, obs_point, 1e-9) + #stop_cond = 10 + sim = mp.Simulation( + resolution = resolution, + cell_size = cell_size, + boundary_layers = pml_layers, + k_point = k_point, + default_material = sio2, + sources = [source], + ) + + flux_mon = sim.add_flux( + fcen, + df, + nfreq, + mp.FluxRegion(center=monitor_position, size=mp.Vector3(0, sim_y_size, sim_z_size)) + ) + + sim.run(until_after_sources=stop_cond) + input_flux = mp.get_fluxes(flux_mon) + + sim.reset_meep() + + pillar_geo = mp.Cylinder( + material = si, + radius = D/2, + height = h, + center = mp.Vector3(h/2,0,0), + axis=mp.Vector3(1,0,0), + ) + + substrate_geo = mp.Block( + material = sio2, + size = mp.Vector3(dsub+dpml, mp.inf, mp.inf), + center = mp.Vector3(-(dsub+dpml)/2,0,0), + ) + + geometries = [substrate_geo, pillar_geo] + + sim = mp.Simulation( + resolution = resolution, + cell_size = cell_size, + boundary_layers = pml_layers, + geometry = geometries, + k_point = k_point, + sources = [source], + ) + + mode_mon = sim.add_flux( + fcen, df, nfreq, mp.FluxRegion(center=monitor_position, size=mp.Vector3(0, sim_y_size, sim_z_size)) + ) + + sim.run(until_after_sources=stop_cond) + + freqs = mp.get_eigenmode_freqs(mode_mon) + res = sim.get_eigenmode_coefficients( + mode_mon, [1], eig_parity=mp.ODD_Z + mp.EVEN_Y + ) + coeffs = res.alpha + + mode_wvl = [1/freqs[nf] for nf in range(nfreq)] + mode_tran = [abs(coeffs[0,nf,0])**2 / input_flux[nf] for nf in range(nfreq)] + mode_phase = [np.angle(coeffs[0,nf,0]) for nf in range(nfreq)] + + return mode_wvl, mode_tran, mode_phase + +if rank == 0: + diameters = np.linspace(0.1, 0.6, 24) +else: + diameters = None +diameters = comm.bcast(diameters, root=0) +local_diameters = np.array_split(diameters, size)[rank] +local_indices = np.array_split(np.arange(len(diameters)), size)[rank] + +local_mode_tran_arr = np.zeros((len(diameters), nfreq)) +local_mode_phase_arr = np.zeros((len(diameters), nfreq)) + +for local_i, (global_i, D) in enumerate(zip(local_indices,local_diameters)): + mode_wvl, mode_tran, mode_phase = unitCell_paramsweep(D) + local_mode_tran_arr[global_i,:] = mode_tran + local_mode_phase_arr[global_i,:] = mode_phase + + +mode_tran_arr = comm.gather(local_mode_tran_arr, root=0) +mode_phase_arr = comm.gather(local_mode_phase_arr, root=0) + +if rank == 0: + final_mode_tran_arr = np.zeros((len(diameters), nfreq)) + final_mode_phase_arr = np.zeros((len(diameters), nfreq)) + num_diameters_per_rank = len(diameters) // size + for i in range(0, size): + for j in range(num_diameters_per_rank): + global_index = i * num_diameters_per_rank + j + final_mode_tran_arr[global_index, :] = mode_tran_arr[i][global_index, :] + final_mode_phase_arr[global_index, :] = mode_phase_arr[i][global_index, :] + + + np.save("unit_cell_param_sweep_diameters.npy", diameters) + np.save("unit_cell_param_sweep_mode_wvl.npy", mode_wvl) + np.save("unit_cell_param_sweep_mode_tran.npy", final_mode_tran_arr) + np.save("unit_cell_param_sweep_mode_phase.npy", final_mode_phase_arr) + + plt.figure() + plt.subplot(1,2,1) + plt.pcolormesh( + mode_wvl, + diameters, + final_mode_tran_arr, + cmap='hot_r', + shading='gouraud', + vmin=0, + vmax=final_mode_tran_arr.max(), + ) + + plt.axis([wvl_min, wvl_max, diameters[0], diameters[-1]]) + plt.xlabel("Wavelength (um)") + plt.xticks([t for t in np.linspace(wvl_min, wvl_max, 3)]) + plt.ylabel("Pillar Diameter (um)") + plt.title("Transmission") + cbar=plt.colorbar() + cbar.set_ticks([t for t in np.arange(0, 1.2, 0.2)]) + + plt.subplot(1,2,2) + plt.pcolormesh( + mode_wvl, + diameters, + final_mode_phase_arr, + cmap='RdBu', + shading='gouraud', + vmin=final_mode_phase_arr.min(), + vmax=final_mode_phase_arr.max(), + ) + + plt.axis([wvl_min, wvl_max, diameters[0], diameters[-1]]) + plt.xlabel("Wavelength (um)") + plt.xticks([t for t in np.linspace(wvl_min, wvl_max, 3)]) + plt.ylabel("Pillar Diameter (um)") + plt.title("phase") + cbar=plt.colorbar() + cbar.set_ticks([t for t in np.arange(-3, 4, 1)]) + + + plt.subplots_adjust(wspace=0.5) + plt.savefig("unit_cell_param_sweep.pdf") + diff --git a/unit_cell/unit_cell_param_sweep_diameters.npy b/unit_cell/unit_cell_param_sweep_diameters.npy new file mode 100644 index 0000000..b46d980 Binary files /dev/null and b/unit_cell/unit_cell_param_sweep_diameters.npy differ diff --git a/unit_cell/unit_cell_param_sweep_lineplot.pdf b/unit_cell/unit_cell_param_sweep_lineplot.pdf new file mode 100644 index 0000000..ab53eae Binary files /dev/null and b/unit_cell/unit_cell_param_sweep_lineplot.pdf differ diff --git a/unit_cell/unit_cell_param_sweep_mode_phase.npy b/unit_cell/unit_cell_param_sweep_mode_phase.npy new file mode 100644 index 0000000..cc1fe11 Binary files /dev/null and b/unit_cell/unit_cell_param_sweep_mode_phase.npy differ diff --git a/unit_cell/unit_cell_param_sweep_mode_tran.npy b/unit_cell/unit_cell_param_sweep_mode_tran.npy new file mode 100644 index 0000000..74f40e5 Binary files /dev/null and b/unit_cell/unit_cell_param_sweep_mode_tran.npy differ diff --git a/unit_cell/unit_cell_param_sweep_mode_wvl.npy b/unit_cell/unit_cell_param_sweep_mode_wvl.npy new file mode 100644 index 0000000..740c403 Binary files /dev/null and b/unit_cell/unit_cell_param_sweep_mode_wvl.npy differ diff --git a/unit_cell/unit_cell_plots.py b/unit_cell/unit_cell_plots.py new file mode 100644 index 0000000..cc7ec1d --- /dev/null +++ b/unit_cell/unit_cell_plots.py @@ -0,0 +1,41 @@ +import numpy as np +import matplotlib.pyplot as plt + +#import the parameter sweep arrays for the central wavelength +diameters = np.load("unit_cell_center_1550_h900nm_res50_wvl_diameters.npy") +mode_wvl = np.load("unit_cell_center_1550_h900nm_res50_wvl_mode_wvl.npy") +mode_tran_arr = np.load("unit_cell_center_1550_h900nm_res50_wvl_mode_tran.npy") +print(mode_tran_arr) +mode_phase_arr = np.load("unit_cell_center_1550_h900nm_res50_wvl_mode_phase.npy") +print(mode_wvl) +unwrap_mode_phase_arr = np.abs(np.unwrap(mode_phase_arr - mode_phase_arr[0], period=2*np.pi)) +#import the reference value array +ref_results = np.loadtxt("results_S21_p700nm_h900nm.txt", skiprows=5,dtype=complex) +R_ref = np.real(ref_results[:,1]) +S21_ref = ref_results[:,3] +S21_ref_mag = np.abs(S21_ref) +S21_ref_phase = np.abs(np.unwrap(np.angle(S21_ref) - np.angle(S21_ref[0]), period=2*np.pi)) + + + +plt.figure() +plt.subplot(1,2,1) +plt.plot(1e3*diameters/2,mode_tran_arr, '^r-.', label='FDTD') +plt.plot(R_ref,S21_ref_mag, '*k--',label='FEM') +plt.xlabel("Pillar Radius (um)") +plt.xticks([t for t in np.linspace(1e3*diameters[0]/2, 1e3*diameters[-1]/2, 5)]) +plt.gca().set_xticklabels([f"{t:.0f}" for t in np.linspace(1e3*diameters[0]/2, 1e3*diameters[-1]/2, 5)]) +plt.ylabel("Transmission") +plt.legend() + +plt.subplot(1,2,2) +plt.plot(1e3*diameters/2,unwrap_mode_phase_arr, '^r-.', label='FDTD') +plt.plot(R_ref,S21_ref_phase, '*k--', label='FEM') +plt.xlabel("Pillar Radius (um)") +plt.xticks([t for t in np.linspace(1e3*diameters[0]/2, 1e3*diameters[-1]/2, 5)]) +plt.gca().set_xticklabels([f"{t:.0f}" for t in np.linspace(1e3*diameters[0]/2, 1e3*diameters[-1]/2, 5)]) +plt.ylabel("Phase (rad)") +plt.legend() + +plt.subplots_adjust(wspace=0.5) +plt.savefig("unit_cell_param_sweep_lineplot.pdf") \ No newline at end of file