From e63c11ea58d6979f43f7e1d8dd1a0a255ffb0759 Mon Sep 17 00:00:00 2001 From: Edoardo Vergerio Date: Wed, 19 Aug 2026 17:48:11 +0200 Subject: [PATCH] repository update. Added metaprop_fibermode_doublet_sym.py which implements a constrained optimization enforcing mirror symmetry about the x and y axes. Mirror symmetry can be exploited in this problem since the target mode has such symmetry. Each metasurface has effectively a quarter of the degrees of freedom compared to the unconstrained problem. --- metaprop_fibermode_doublet.py | 88 +- metaprop_fibermode_doublet_distGrad.py | 692 +++++++++++++ metaprop_fibermode_doublet_distGrad_xy.py | 900 +++++++++++++++++ ..._fibermode_doublet_distGrad_xy_epigraph.py | 954 ++++++++++++++++++ metaprop_fibermode_doublet_ego.py | 718 +++++++++++++ metaprop_fibermode_doublet_sym.py | 667 ++++++++++++ .../metaprop_fibermode_doublet_distGrad.py | 717 +++++++++++++ .../metaprop_fibermode_doublet_distGrad_xy.py | 901 +++++++++++++++++ ..._fibermode_doublet_distGrad_xy_epigraph.py | 954 ++++++++++++++++++ .../metaprop_fibermode_doublet_epigraph.py | 659 ++++++++++++ ...metaprop_fibermode_doublet_epigraph_ego.py | 780 ++++++++++++++ .../metaprop_fibermode_doublet_multi.py | 676 +++++++++++++ .../metaprop_fibermode_doublet_multi_ego.py | 799 +++++++++++++++ .../metaprop_fibermode_doublet_shiftSource.py | 611 +++++++++++ unit_cell/results_S21_p700nm_h900nm.txt | 25 + unit_cell/uc_center.log | 31 + unit_cell/uc_param_sweep.log | 31 + ...center_1550_h900nm_res50_wvl_diameters.npy | Bin 0 -> 320 bytes ...enter_1550_h900nm_res50_wvl_mode_phase.npy | Bin 0 -> 320 bytes ...center_1550_h900nm_res50_wvl_mode_tran.npy | Bin 0 -> 320 bytes ..._center_1550_h900nm_res50_wvl_mode_wvl.npy | Bin 0 -> 136 bytes unit_cell/unit_cell_center_wvl.py | 169 ++++ unit_cell/unit_cell_center_wvl_diameters.npy | Bin 0 -> 320 bytes unit_cell/unit_cell_center_wvl_mode_phase.npy | Bin 0 -> 320 bytes unit_cell/unit_cell_center_wvl_mode_tran.npy | Bin 0 -> 320 bytes unit_cell/unit_cell_center_wvl_mode_wvl.npy | Bin 0 -> 136 bytes unit_cell/unit_cell_fcen.pdf | Bin 0 -> 14549 bytes unit_cell/unit_cell_fcen_h900nm_res50.pdf | Bin 0 -> 14537 bytes unit_cell/unit_cell_meep.ipynb | 902 +++++++++++++++++ unit_cell/unit_cell_param_sweep.pdf | Bin 0 -> 38133 bytes unit_cell/unit_cell_param_sweep.py | 203 ++++ unit_cell/unit_cell_param_sweep_diameters.npy | Bin 0 -> 320 bytes unit_cell/unit_cell_param_sweep_lineplot.pdf | Bin 0 -> 17122 bytes .../unit_cell_param_sweep_mode_phase.npy | Bin 0 -> 3968 bytes unit_cell/unit_cell_param_sweep_mode_tran.npy | Bin 0 -> 3968 bytes unit_cell/unit_cell_param_sweep_mode_wvl.npy | Bin 0 -> 288 bytes unit_cell/unit_cell_plots.py | 41 + 37 files changed, 11478 insertions(+), 40 deletions(-) create mode 100644 metaprop_fibermode_doublet_distGrad.py create mode 100644 metaprop_fibermode_doublet_distGrad_xy.py create mode 100644 metaprop_fibermode_doublet_distGrad_xy_epigraph.py create mode 100644 metaprop_fibermode_doublet_ego.py create mode 100644 metaprop_fibermode_doublet_sym.py create mode 100644 metaprop_multi_objective/metaprop_fibermode_doublet_distGrad.py create mode 100644 metaprop_multi_objective/metaprop_fibermode_doublet_distGrad_xy.py create mode 100644 metaprop_multi_objective/metaprop_fibermode_doublet_distGrad_xy_epigraph.py create mode 100644 metaprop_multi_objective/metaprop_fibermode_doublet_epigraph.py create mode 100644 metaprop_multi_objective/metaprop_fibermode_doublet_epigraph_ego.py create mode 100644 metaprop_multi_objective/metaprop_fibermode_doublet_multi.py create mode 100644 metaprop_multi_objective/metaprop_fibermode_doublet_multi_ego.py create mode 100644 metaprop_multi_objective/metaprop_fibermode_doublet_shiftSource.py create mode 100644 unit_cell/results_S21_p700nm_h900nm.txt create mode 100644 unit_cell/uc_center.log create mode 100644 unit_cell/uc_param_sweep.log create mode 100644 unit_cell/unit_cell_center_1550_h900nm_res50_wvl_diameters.npy create mode 100644 unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_phase.npy create mode 100644 unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_tran.npy create mode 100644 unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_wvl.npy create mode 100644 unit_cell/unit_cell_center_wvl.py create mode 100644 unit_cell/unit_cell_center_wvl_diameters.npy create mode 100644 unit_cell/unit_cell_center_wvl_mode_phase.npy create mode 100644 unit_cell/unit_cell_center_wvl_mode_tran.npy create mode 100644 unit_cell/unit_cell_center_wvl_mode_wvl.npy create mode 100644 unit_cell/unit_cell_fcen.pdf create mode 100644 unit_cell/unit_cell_fcen_h900nm_res50.pdf create mode 100644 unit_cell/unit_cell_meep.ipynb create mode 100644 unit_cell/unit_cell_param_sweep.pdf create mode 100644 unit_cell/unit_cell_param_sweep.py create mode 100644 unit_cell/unit_cell_param_sweep_diameters.npy create mode 100644 unit_cell/unit_cell_param_sweep_lineplot.pdf create mode 100644 unit_cell/unit_cell_param_sweep_mode_phase.npy create mode 100644 unit_cell/unit_cell_param_sweep_mode_tran.npy create mode 100644 unit_cell/unit_cell_param_sweep_mode_wvl.npy create mode 100644 unit_cell/unit_cell_plots.py 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 0000000000000000000000000000000000000000..b46d9806ba6a992787b3c72565a5e36e1f70c729 GIT binary patch literal 320 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$7COVor3bhL41Fl&!L13r-=fpefy|wn+t4-y9To-%DKIo^xyX&e)?dxMf ztmF1;4i>X?Z98RuLlvaptUZ%!H19m+3-%JyZl zs(sOZkj!=ajzSRQhCNvH!A<)uS07k1x!$%v)ee%lV-Mm*^WL-n1~RVjzCHVSL5*8k f5A212{h3$nJ38Xsc=4basbpjGm^)I5-@p zE!w9zZ!U*})W+MVXWZpDaN7Kt%<1dw2ef%>-?&w<9VmZ$vM9TS<-imEw4|_3#sm9I z4YcP={MsLt#JE2D_{;q!^BuSvUmUh)k4VZ$J@DSX)Z}BvCe5GrQu7~g+Gfq*VDo#$ zE8PwM?90yVij-_&cHmK))LYoa>TpTI^Q&AqyMwWzp3Z{L90vrp`dj!naU2j=t`9pm Qk?p{vU#h&ln^+D206wB{dH?_b literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9e7bb1f001b6fa6ddb4366598d0588618280696e GIT binary patch literal 320 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$7COVor3bhL411|Zw7wez?d1rt00K1|m`+NHYlZG!nqVMhJfB03=rSabW z!`7zIX-@C$#s9PhTcy6Y-^U){lhXIz-txhIkq3L;+Y8*O=J$W`-ah(?*DA}G@9i~X zr>*i!e{UbCWx9E-)jRvO=NX?Z98RuLlvaptUZ%!H19m+3-%JyZl zs(sOZkj!=ajzSRQhCNvH!A<)uS07k1x!$%v)ee%lV-Mm*^WL-n1~RVjzCHVSL5*8k f5A212KBIcj?&*otK^mKZ!6T1WV?zCq9IqVLS;@2WG`8XU{ z9d8M0EaY%#-6Y}Wai8PBy;*KoKi^QIakYf)0FOuO1@%Uj1G0yh{ohV!JYav( zw)mdGul=i1+5{W_zT6+7?QI!4@vyy`z^AK6jNaStKEq^|_3MXy$&1vVa|9V2I3wk3 zK3)H7zq$EhzIZOP!<0kkRdfqk9V$0$cvfi3?$EdJ)>U?1&I8tMMhgd`z!|lo6B%= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..99e1d66c8f04b35bceddc8765c92c3392f06d064 GIT binary patch literal 320 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a9160852da919dc796f59ed241fc86468e52db64 GIT binary patch literal 14537 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 Umq2nwa3m6qfd~ny9aV??A7aYz" + ] + }, + "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 0000000000000000000000000000000000000000..426c89959089b028808af9f64c00c53a5dbdb0d5 GIT binary patch literal 38133 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 T|FX#9a2YIXSJ$bbJnMe}!K_d? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b46d9806ba6a992787b3c72565a5e36e1f70c729 GIT binary patch literal 320 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$7COVor3bhL41Fl&!L13r-=fpefy|wn+t4-y9To-%DKIo^xyX&e)?dxMf ztmF1;4i>X?Z98RuLlvaptUZ%!H19m+3-%JyZl zs(sOZkj!=ajzSRQhCNvH!A<)uS07k1x!$%v)ee%lV-Mm*^WL-n1~RVjzCHVSL5*8k f5A212+WKqWrYSA z0c>UD08y;G(IBv*6Ci@r&u{6U--;lUU&Mj_J^+l|80p=u0O@{+57xBuaPf4v0LFun zAKC!+gaE$?0SLbk2#!GV3jp1NrGbF~tvx`fAL2>*+6J89K-!Ha}2$ zE-)0{IdG@siwru2l_Jx0#^8*Fj1!1mA*^&{f8WM)WB_h5md}1*-t@T6JQv2w%KA30 zAUp8u7gKSuhVMIzx3Z11C z|EAX4m%Puf(?o1WEHiDt68Ut61SVD_ql_oMg%2?*0Tp*Q`XXiT8{L@V4>m7)<<8T({2*4m1}$yo6v86_m$FdyLnki ztby@L16_8Yy}ZK6jrep$uk~HQtmVK-;)#wKwt~{vMjeAi_qF{U zt#{vdJxv93PYl-Avj#jY@JGHBW$qB4fPaUBKR7?%X_pHO^b%}hRB|gIaNqtoarUFg zZPa^keLGY1BRVPR*+SMFTWRmrbdxlH28*;3?&eZN}Z zz?aHRx|ToBoJ`F{HAdi2112^%P!;X1{FESu7glzA4Q9wtJkDUh(>{KVs&cZZGc%Y@ zvdQ2@xGTu~W74%=N~03g&f-4#cKU^^7%enThHM=DD^KRE_U9%_+#;rHXU-b94CdX! zk#SLf$+Dn?B88C)P$EblU#}S%qDLA`Ed)q#-ip7tDAB1|_|9UxeaL-5;G=ryyh7Rf z#uI2xWkhCfir?* zvY+3#gT4vxgS8q}5+_$JW{Nfv4u`2v6KmTp^L=?)EVa`kL3GuE;U7y5ueYu|XDr}{ z`+s(y1R(lCl?&nP@cG>Lk+hiXQD6uVC)xrRf`D6DK#?ZHSWbEJ}=mXExhWg zRt41UmOw=+Jn?X9qlONIz3`z*)2s-l+$mJHVPJ8dstkbc4H)DI=l*%hAuPKHzuZk|3MSm(KPV|b} zW^l{f0D^#NW0h#Jg^kJ@b=Tp%Cb36%Yo_QR?V1M-Vdvk6OstjBbA%7!2c?(2>KEWj zahtb2SZ&wYa?kVI+?qDxtnzv(?^@YK%n;w~`&A)gXr6I>_y7o>_Wu zYHB~>fJivikeqvqSUHJ`Y~Df8B#y20STdC*$<`_Ru zQN7dkr{~`)C4J;^4-aof0?Zs5L{wApfa-}letKGyQvS6zMOH06(uHD+9^z@jF-MhQ zBwOAF9zmETJ9!K7RZe%Tw3{thuvGy|2fiITZ=y(7`_8aib%u2232j&J5x*QAs&;iu z?jFU{GsFq(CB|b=LPWx9u^{}jDtUh4-1Pmrt!p@t9#>oycedv9Fo( zPU*6;b-FB`d3ys<*Ck5nhr^Pb?OhC-TIT%t<4rftDhkNSDoTKd$?c79oY|Xf;`9lI zFO5k=cu1^saUN~)_MQ14of?kr?f#sv+mTRc~m1hqbO6c zv@mgl#wj|R(+8nSn&gnTEIDaGOwqRjrdvKpo(lA9&?R7c8DzFD=AwJ~j0>t`X{|v` z&MvbD={e%b=Eb2)XhiLt$V^`=uNUD*PPfCfA^2IWs@1=Vj?r7-j|z6*F=v57W)T;aBH zYkGfz(|5+z^ULDdj+Ly74=){d_7U&)4K=uI%dqs7`sc{gB_H1PZDMosc;b`WLpC-n z#c{1LveNxQqN0y{bj-ThXg^on@M15*o==H5)2`}FWLrZA%ge|eNSCj#FWgnc-nRTh zRfXJ>3|9z_Gw;_^p-lR3$oGcsKdzo^a8FDvBmB5qzcfX@Yj6Z$KmE0;8 zeR$7_`oZ%@Cb7<5Rko`4o(+yG3E4R*3pMo)PPkPuJU1a6HEElc+AF;kL8yo;5ynU+ zsZ;c1%ABQ)uo+tZR?8e(EU&4pXGEQ|o_9};2NcSR+`fnBGZIU@&#uQ#KgMV9lCLOp zzusnmp^~UN?>nUW!F6A>U(U>@K(yc8cXoET0gcZd4wAZv5Fzg4zb)!h_HxK%QOp=# z7VO$3Pf8J-wkTgtKRxeUSzTblR9|E&jk_z~_wf=)M*;q{^%>uKoy0VkVrAftcO0(S zXj6xjP<*$=e$&)lp=k@$`Ih?a=@c&zLrxQFpEGWs%XEx={e_>=c(6Yk3-7@U&1M2M z_x_hd|4s3%%`Dc}54PnM6vJ*OZ&_Z7l(nczS{g$4yvayVla=giFg`A*~eTT-EW{lr-l2P6K=c6>PL!_)!d}> zFGX1=k;rt86guUb4X2~Bi@2AQoDx=ro^H5LGJVu|$MV%A?C}U+SXS%Y{3*eW)01;- zF4X$_&1E_d4-o6E9FDC?$vpyOMyz%ki&jX}*06>zU+m{=TLz#GjS*#$nfa0|danfL z-vmXc=ZzUAzw*mFTblkzV}(vj#RE4tZ(J|Eoc%$Nk^SVbs%1Y==F<$>kIB3rLgJ>X zy4MiH*Yy#E&c`qJ?CuGXqUf)WnxKyvDYjC6=*(joZ~833c4DvSj-!#$z!`on5!I3L zSx4uhNd9NKsw0jAj~}>myw;(#mC=I%0W`f1PR*xpgNGJ#fQ z>?gdObY{8pg+twv@$1B0ZK9~v837Y%9Qhnb(weqI`V9xAj$4KU{kIH<%ZNvcBHe0f zGVWwDW)IKE*pZ1lw#t|eY6tYYs`?-u3y3#`kQ_4+yrt(9-3Q(-ad-|)X5LaNY&Z;Z zf|l35@qtwi$Ge@OfEY!}MI?=jftmST$8v_nTqGY%hNAjTmF>D)h|zX;*OxV<_p~XD zhw1HkV>gUCG%HBOr7oo3G%ME18k>8g(50PNZC_{8IwAD{Z=RqrkT(6Dxe;5b2{VI^ z+vx#zIXyd?Qk|E%4>$NrnjN)S6V3B3Rupd)MI!Jgt#m)Z66gj$_FcQ298R5x%w5@d zMaZMG>u6K2r&*IAAk$M&UvABhd}FM9Sd84%fb$PEzU&eWi8$N4MBg1Vnf!@%ig<_W zJUD2i_Vb3ov(bI2>VOWr8iJmoyCvy`WKCqF)qUbiZo2(Ke2E0}Hhh=eZ-#)?wyAP;f6%C$lV`biK_-iGm)Ge9`=BiGC3fivNL(fx z8qKJ+352T0W?`^gU$M-@CF0p=k1AzQTh0W6uRMyo;=I-cTQ0VX#Nn_YON$JzgiG^e z&e?^*yNc!lk6#nKk#a!0iXh9`)E(~Kw=>Kbek1T`Ca(u(+hBLw>lEbsRj)8Z2o5?B z8=R2H5VU3J+d%hDPqknvZkEL7!TAqP(g|aG-q1N?#0U7(i_$3p=1oBX8`-L%mTQ^K zQFYzDT;g`E_Zo*A(z&SS=dRZkgDBu~UYG5M--ItmXt=)mB3J0wR2<27T_)|#n@knB z#rZXPiZcyS6l@x5GddscxPdRl&o)Ny#Ck(7XihWqvG%Vu6hk@(ap_I2yy_P4*(?53 z-@OoT?@N(+En<@CuIMVaxHIki@PXX!L`RG@@wo5Q3SS>g-p{^Z9-4ky;o;SAAt&NK zs@piF{Gw#b-q98gy`CJ!A){MXAS&>sp4Tnt?eGFD)P>~1L(gv|lOunMOEBAbpoEN}q<-ON9iP(KC zf49JxbeUzBY`gHZl?lJ&$5Y))SL6&*^W*|l-p9-ZO$;h)rhhh#drK8#Klw!=@1k4Y zYrbt;-s&>VePxOsWX7Nra>-}u4cb)-lkGiwgr!E=js2{XDbS-{xT34eQRSbD!JNo)A-3VWG=G4K=PIKqR zt+|;f9adaSIN;}5_Ge(==-`Vv48sKl1^ z7L6An$7Uvy_EXDY9i2Uw$(g1S)$?sae_D}_x7>%^k`TvcB(b=Y-Fvk^34h|#@?WZ~YeP*`#-Y~NlhN{yIgn!tMbB)~m)HKJlWq!ERE_@i%ysTq0tmW1z`ZNG7j z^kqP=n0nU=ZT_wn7$!b)Y_vk~{~oMrPY-bu2p_LsL<QGE|Fm&qB@NvwC({cW! zx9O;e`dC(NwYQ4mtDZE`%a|8m=5{EKaC+O&dE1AgB47o4pKR$?cm?CbXQuo@OyqAE zLQfD46Ph_Dnjq>=L~AB!g5cx!*J}xd=*HAt<`!i;Fugc1VEpxR*rQrzGdbdU3F?-l z94~QlY8pR>rxCH_%lzm0?V?q4x32_hSUpcOJqV*OI|HMj`F?^}m=Mu1u~4u-5UaPA z_(-g5ZklF>rBTi(O`(HoXbMHaocO&UNOJ^jRSj(&;u5~~W~Mi`gDKrTsZJIxxe6QS zZ})XYy1=T+bYYL#nvHJPRTsSa(B!>IQZws+Y6r}?c!G?WxYaQkp#p!PJ5hBMc`Jx? z%_ltjlpofn8%I*e*XKR2)`=P9Gi(0Hrk5AV$fafJEAT&A*lHqRpS} zpX+NB(>r0rn84n#5yRkrU>4DM$wX`@KKAA=p=dK6)<-{Uhl@8B4oA+bRi-8s)5vty zVq^QQBpGlqeM+NN6e50fi`Mm=E4#mQ8~&);q@I4uE3(_}AQ*Iim~9a+TW>)~3AayY z{T<}8x?;-NGB%^3*i`=5oQM2xEVteT^q6#r#;r9-`{g@!)%3*WJssD;L8?9uFR?e| z&!>|uZoS}=r8*Hi#Y$CFIEPzYz@e4Z-d-Sx-?6W=vF4_)nf6F<=8L<(@wFs}3&rPE z$A&Y$F6`A;?!3#GuR-iFRrq+h3&`KkFA{#@@-b`p;O<-Q+!^tA26{9nDD2lr>CfPx zAmqQNF&vK2hN$-=@yM+sI0%n1LLYw z*nH0S08s_$T;xpMWB zS~Z;#4_o?x4=3Dk%gRY&EtJW*hg9fUytJc)u$jj)@!pwsr*nTlq-!mkHR+qz4q? zx9TucG&4E;pt%(Y>nVPYo6~Z*Fw09tyj^@qC}Lj zOMk>yIGj6`hzpxJYaPPg$snTPTakAYMZ^hBZag!yUTsja{s~obS)W##r*~UtuPyg{ z_}H%VX*zewO`TOd?5ypZjkuPq%`pdD&}-jmIlopu%iArIP2l=YkF;16AREXmGbhX; zXcrbCm1{yyQ!$AYA+)cf1;1OMxtOVW8u=H zFGH%E&r6A2>S&tTUeILjB(gtQOf9rz9GlxQVj5w+z#hmnF`njgbH3_H`$%$Mo=s#H zscda~R!yQ{=VR@g>Juh;U9Qq8G1&1`pZamn?PT^IX?VpV+;ke22%rlcJD~Fqm zvN=6LmYk5CGyHj;f3|oWxBBu`URkE+SBZyC=R9RI$M$^foXYTCx>Q;?Hi7cHXR`L` zbB-qK$g2)1>Pe&7p5k&U;ef$IZ+bjOt~@k=Ju#rYuWEB?djdw@7+qE~ti?r&KyD5#79P#)U-R*ibK4Bd;-=Ybnv76jb5u^%H zq8H+j`jEON{<(VMwR728a$Yu{di9gp5fa0^4rUHpVt9!GSk7h0E`vFw3+INU%xI^@ z0=A)t`!h1*-XR&=Dj5=oj+@twJ4x1a2*#0%7MZOVs4IsL)DChg;cy=2knzF@`oA0@p8o*sNOOzjs2R9M!ra4#^fkrSN^`3(;l@RMOH~)Mo{cEdKyipnq={E7J zAB_wcH9;aM7bWlqT8xn-0bn`KW`p@%&^?L6L4@eL!$&#} z7GQ{(K*wyunpt}s%K1zcU6mOF zU-s95=N0B+ldH5@BiL7lSrI45c>)-Q3jBe&Wz}oRS|LPhNL;zuhc*|M-MKTrO$6S6cGM zj57>);CS5(b6HVkG73`nTk1`?9#U4WHhfo?&KCJzezj*zg-DXKDsre@47>K5lb5AC z)x5mHB1+KRaIhyAPOe=LtEsrZZX{APwo%HkJ~%DTx+?tbgu$MGss;bR;lp9o>JasE z1QvM&RUzS$^-sw3AhK49KQ#w8EE(IH_LQBn5H4BYDX8Q)C`yv|ra7Dfep6Ton z&)puzrSwwDOlm|y8hHmsoefn@c;V1pU+F5&aPvZQ0dI{Dw?k4DGX2Tv7kd{x#BGJe z9dpFr);gwZ#rJe^QckIBY)02sR#dKQ1>abf@#RWUZ|Op9-2f?zcGgptyL$z0vDrr8 zyvZT2=@toIe?vet$SvM#sdn1rwMrK_Nju>@$w)kLc$TFir|o;3_E$cE%ne^3eus2q z?5oR(qcv5Fo$QA0R%S8!Nm8MV%J1x7ab#LjFv!?#ipg%YaAzKc%-bw_Z64A++CDh9 zch!2~1a+POexZL7agYTf4y_;}89%iO`To>CpUju}t{XGf;T>*R2mOSK{h}cK7CsB+ z`nLt8{U%#eY(hp0dCv2N6{OwxS@;*d82G~OU*Y$pyJ+Hz$Z$w1fLSzklTEL@Qwn24 z^E6BpRK8-?S^kh3y=uCBBjj4IwK={nEJMNMJv_d?u_sog*WBNh+Pk+ja~Q3KmJ2Bx z(iC24TpMN@a@>D(w2IgZMNa=dTEq!x6At@>_zF@}*}WZtKe0?{d=I|la+fnpje7lC zb+SU8_qphBHrH3mDYswVD6~vQ*Z8j%)Zrx0a2jxnr~1Bfdu>=LjlRE^asWFu-sWl~ zPgIS+zbIg{8J|bKcGLIXg;_m>T&+IaTdF{pQ>N^}YS{I6$=77d>?>q&>8OZ@((&Ea zUZV4J%-G9f>Gk&2Pa`;#ud5Zit#NVwPpr+QR@#(#Ta;-@D0Or41yZCTjpKp|(*g3Y&dDn~FK* z9pp+@H=XrawQSVDQswHcs+Q2Ys+aI7T+D5_iMK+Kg}irEOX-FscO#De+3}$Vw6C_3 zA@jYF-K(Y0G7e57`Dx{j&%_fMt^5xJ$miDD$rHtElz8Q!DY@HmF*_M$S7daaIyvx3* z*AZkLQqAl*S1W{l{T$@XiWyyqZ1?=}!u#?lCzaJf#duha_;}wc0WkkkAX7Qtpc6{h+s^7O4 zA$?kLg~F?4r1|@*6VwC*ZjIiQIv1T$njI+Idt)g5hXkuL8Q|xqOZAOtNx$e1)XK9*HOEXMdB4hP!Q)Ser6X~o^u-^%I z3;HLUfav9BVj%8uU_%IY)g%gw_tDe|Fy23;;@33QA05@#lb0tl)(%d*V=>oW`2O5i zfo+0wp+(Ie7qyA9Mq`w(I$tHFTT?NFM#^|35f?i> zunc)bxI%QwE)MXLl0b)r%y7+F40iJ_6n_XqHC(=(b>kj+@xb@g`ic9`0?n);CmatD zC%_;m68hgb2zj{{7##*)P~+e0dy0q)1+`lp3M7IbDBOv&RmNkbIR_2jZla64uP%x_ zh>sR}&$1X!OX%6kTLy=8mn&+`C0*Ps*B6XR9Z)gpC*cmXsSM}Wkh>=KLc$b9A@Hrk zn9ZKOjGQz7D&NqgYNCdfPB+1mOWt=yc*)nd+|ib!t16xAf$fXLuna zCs^+U`1B`W87}Y`Kok%J1UpeKLLw#Mfm9T8YECdJ-Yz%33zrO`(TP<>6ogP9k(0KB z0o(XoG4Y)fxSxP$paOrOU0HPz5@}|l2~>ETL}UDJL-FdC@8J`Y;&EJ~3O6GDkjV5) z#*_&mH;9&p2DdJVWt@+uGQ~5>xKk5hm8d}wx=Bza>`gp%0Qn%yT})W5n;)WhwlaKw zso6_wY-;|(htMtAfnD)kZCKX(7B(Guu6-F0Z=ZFg`kjwjquleLiL%YGm~$KXRfgQG z7W0boyn7R&kt3Vb8{2e!=j4+GCr%jY2}lVJ{{uXYOssXnffC7j=rrRI-As-3eF0$@ z{dWKQLRlyl^7bb%x2ly=guktlW2!OFU11jex>qibSW>=5_p>JGeJySietVHd(I?^{ zMTkQT<)-O4Q`QHG)3bq9#c`QJ_KA9M)1&a(#7sSX*UR1e5sb0yb+g^sST9@K(6{n> zsSX8hE%Up1YJW*Lr~Xc`mUh)cOj+4Z@Q!(C&*w6(tM_%2vJhsM9+%vHv>I9;$=Rlm z!{fLao4q=lbDEbB$=U0%s9RVXUCj61F<9H~f=!F%p;3!0b7DjL1I_D?sYtG{6YoB- z&E(>r-Y)o3rU7|YoT&s=$xin1b2VmddO7SUS)n-h_|_NYd^@X@`gU4D-zrxB7ukHw z2~D0lD_+CWcPd|Q^*&AY{j&6Ge|4g_MaEA@yFp|llP}Y1=xNwso7b~4qL#qt4L1I9 zkHl51{BNFMm=lns;2#_x0OM%^kQ#EDys$9he?%d9yMuXI&&H=P@JpYyfH;{u=3lio z$Fnp5K|ytxMUg^5esF?BCx9_H>`#JWnkZ*rs;n)a{|ZDRAAidxrs7UKtY&s{WRKz1 z$QNRa37-B`Db>@~-Bc|~hK(W*jC&-OmdZRF;m2%0_hEDz-xT8YDP2%*H9Ub)Oj<)YlOqkEk4h@{7~sc(p%JBoHcQtwu!BR3;gz~F-st@cK{JzkJN)IRK0bd4-y zLm;uH2(`nR`||y3e&=PK`^3`R;$Ex1GSHM_PC1+Ov4Fd+!v0lEjH-5-;J`Z}mKvK_ zSuMOkmV{jjAKs5V%6j4RFE@)UST0uHxWsvRw?ba`*&&t%hkx|%_CTHh7omR;7(Q~5 zL9LY1Wr$OZOW2wx^n3#Jfdf0ve?I8r`)TQKgaJSRZ(?%{q)_oWkY2N@U4kKta&ae- z&t#{7JB^Q#f2Phn^^@mkqXhoz#&4asVT!VC)eX8VZ#ErroVeE2Hywww!btyeDSn%P!JZr_uS4fsfNd&_mugN@QqB`m6@+M7#^2qj8Y??}IQ zf%4s1^oLM}4DtjSQaGmpb;S3WbBq+LaqY%C*utH%a>1Tg7c0k#qzS7S^h*5ohtO5y zac8Kxrmc4G>S-yc*F^X#<&d1=ou>(k=#M|25HT>`&)`7>tM1E+pHWl%tWGrYsKPQg zw8n^@lC`SKB0o`Up6@FEmqBaySrAt`9v_|e`~xnxqL~-Rv4~H)GE^4wJRAYT6q%G&d0wt#iam{m#iN(>$H+lR&Y-lF0pq}(&(mE~J1Qjg5a7`Q|O5}`c@ILV3M&WO$rpK&5e ztr(o0Y#?XG+E;x3ZE);?OJ)JQs(o@h%eo}40()MxRJDnl+oq^}M8WH;&x`%fK8=T1 zSF(&wc#euZ2_g#q!GTLPUfEd|c%Z#p(n4QD-g{2Jue6xt%D#B%T)YatBs|yQroEw$ ztyE^x_H>MZeNvix(Bb^G1G};&0}}_ILFtWsrUr&ft0zpK6Tl-3cn#n`*EdqV)=3ud zoz~ zW?nRn8%}7FW4;CvBBlUKWY9zQqUB=f>RctYQq^*rcE~>o`+NGWT(FoeaOy;n$q=!J zvKOTpBFtWY^p`f=ov=xE$+X%R*qPL#1et?19moy9Zj@*32D*d9uzO52(||DIY3}*J1>Cl0muvX zu=28U27#@P`T#rI0SyFz<}x6#EHE&TAb>DbK|%mmb&wFCTTPG>0yD%e?h^3 zcqN7d=7JA~f#lL=t_nZ21R(_L|5XICYk<5TI}1r?8%HZZaIhBI%1IYfP&M;D8Y7@8 z#IK|I3-SY21?Hjvpuzw3fukTG%;)(1zuN&~3BUyife+^Ue}DcjJbvw4P!Iz21p^@g zC;Th@*Rl{W_y4*6Sw{V=4`dbna}Vh2PxwNA=?@O{1O0_J907s}9_i?)FH9MuOaXu{ z065IQ`bPkKk-)S$x(4q3^UBn(7)=Ir^ad3$*JMFQZx;b2IS>rvjDeCW2nh8ZmDE8n z%=r_bX#(7Tl(a#xBe#K>^T5nFx@!qKdOi)*vjQyngZ5*704hAHakR<+5Aovyv+w}n zfD1xkp#VcYBHMug!yT0zLGYj4oIvoOPRbdu|Bt&z3-c&M2{3d4A&wXl9C<`3E?@#QqwLN_|U zUWPll(OdfZW6mdEMw!BdjWZ7l;bXE+iBoE{bf0SOPmAu`XxedIxfb!kt+uT8ec$$V z&-|Tu8(3mCQBT>(f}dY*{?t~r)_T(nKeNO46$5sw4(2Z&HDqG7x3_OS@Kc{qU^W}Y z3%{qXjjtX2g(Y3OC7xj6bhre!I>FHCm>>kVR1*1%$`lIa2RsE)1~f!O9CSoQgfS1j zZ}Cj#N?i++SPX74!w=?>d_!{{i;|VAW|c>C;I0-IO;zl*QmZB5Zes^7Y3;{cazQwF zpXi9*P`-6oIGm#-bRKOhBOc$L|JF!8O{RR!h&*6Bd!NjIT@5%K*r@*F zl}@;N=UbTi)@gH#aASTG3tZ5vxe!fSV`DYrQx4U5gumPDXas*E8sXof3Gxde;lRsz zNQ4jy#3#s)fC)m75R?$$>;84?SJ_dh|CZe;R)jD}f=K2dlZARTe$%@8$sQYv@8NI| z2!8?xmL^EiPC;|6$jn>XV%2UgN<}M4dMl(5(ZUv(P8x@5tB5}x_CcpAL$<^G>n?0q z@)BW@=Ze7#K3}~<##8i{D6lw8PZ<3#li+8%?*B7}`DH5pUq$=>(lEasq<<`!AKBAj zWjjj@?mbFV*8GuL>ATjyIWZkLO@6XLdf4gAOR#2*!ZnMf738N1oH!O z{z-!(1tEar`8N$nvj#lSaT*3V9HR*W2>9ss}+Je%BR3;CG!O1O)+fbgV531O5I@LqdUL)^QpF zIBp%M31HyrzuN+!_qYB8;FxXdSUrK?^b8S%{@zv)fq}38ZYzjE*2if=7`S?@9!lu9 z@d^QF=HvB*5Wlw-5&%ve$NED-08n_G1{XL!uOKK4`afO|7|3rll)!PBffFq1cs(ct z6IlOue^5Y_<9vaW?r%1NLSZM+;K1uR$NGZ`0^8~_8VvfoKLF02$88Sy2;da$rgrcn?y6?y#Lqri}NC`zEV-cZDC6OkCjL8r(M)$We zB_~2;p66-XX4|%Zo`2z4@7DEVy;*BrpDX6fc>^PJJkAq$SNf{m?JI85s^ZdW*N#cc ziA!H|adUUOeAC*+?W*1X>Hl7Kx^1_m-*&is%WkWeQKHF-D;?S|C+;Wy|17S4nz{BD zyJHb}+I}OsCgdI4WiEz)# zg2Y}sHVW`k3IvB6(fE%c$@4E)Kyb~?^G>ByERmruARA z8D+^{>}c8c8-xoVwKTkpgK*oodHP-=_P|ACap=(ma%Y9rak$NZZ_fr-jti4uj?jhO z8;zUD#AI=S8B~w{6iv3{53T}na51RRiHgm6-pm~}UqTNo(+PK<{{jE(GNmn1C($y8 zFUnn+2Bmi9O?;a~c=6Kip!?ixq|lE2a5is1W?9$l4=k+!v4Lqhr7$XXU##gPK6(iO zxu+wq%G!a}EqA742@clQaX6SX5U_6VcND8b<0z%uxGBDA7MPq3%y4igK>_{YW6wl4 zQKs)tf-1cV@tUvvdNjBSc8&xu+455{g-gfxE2J(Wl_n*F?QO% zlv(gB`0P-+BMI)+zM0XZw24ZC-(B(xtwv!7-rcKhS_S-H3LH`dshGiY2?6WEMbsKn z#-*d$1=1dMi~K8thebBJTzM7*EO%cuHTBvkIuYTT`t1xI9Jz7Y#&w(oCG-0m25Br5 z|6FlQ?7$~P6FPq{ym1Yv2JmO|#8I&HJhImB!Ua^ZPi(0{r4Q6w#Uk5dqexg-Ikd)} z4oXODddCt8-gpwq4Fy=}tu-!1QL_vwdx!Q;%&mc-o*(?vVH7N@-q9i9$pSk0!u)q} zb06S*mhKx|frpWb3h8Rq92oy=1;q{SVH6k1BuCzx2kowuhq3p`FlpT4>@M5}s=z0U zQ`_F43_aI5Z%!tl)W{UoZjiC)>vhr@?hG`?;^hq&9|Vhud9v{qLug1WDORL&9_-#5 zn)K}h8Ok(i6py`OBEbfA1(J>z^4YGc>2Zq*F4Tw)G}e-_Bljd(Sri5;`L&Zb<|89_0Xv) z0I7!k%dSNmpy;DRM(Vz;x!uCj6pW6VZ{>+aM~{HcJk08mWgmLl8*)vzXaStOXgg0z zra)sxKj(7k8d5e{`Z%Fc3BnD6}h>&o49n zS;MJKz~ym+`#70|@wedg)mEp`Wc2z-rOYI_H*H#xU-uiy9@`6(YgZ=ojG?7 ztl^-D(x($0cL~_?M-!{SqEYmyr(Wfa1|8^%SY(aSzM!R&>SXA*1|m*rb?_BZu*@Bs zJpW+}=)7{o&zrG*!1UKQ!oeCmY!7_d-~NsRi*qdh)_#5n1@-b6S8dLNlf~v8g{@@x zS*I#^T#1R21g+HroZkTN3vy3u68*>QlvXvcWzS@%|?9>m{$)fG@(wlR5}Z-fKoTs3n@P;rmbh> z;V-*{%u0+Rl;3m$slVSBEvw_<=&OTGae@R4|94i-K*=!D-Vt|X%6}eY_#}p@3 zervjd4ignB1VtD4r+}kuV{Pf64e(*Nm*-cNAt1WL*Zz1l2h^}l(L4DNkGY2@MEcou zqnWu#Tut91_{vRFPIsol!6tmWdCm%IyQR#OHTwnhRu2@pE8t*Is%hdV9})XnwZje* zA4gaCIX~DC=-|zv;J!K@GHmUX%t|oaK&P7@T4xD{pz7y2t?D>i4fts=W?lj#{w9tu*>qLQlSIWdBR$C6+LhHV2lYjf$rC9 z>Jw5p7!nvRe0YU`-Jzt}>m46M3jVbg;-z%3+N!_HHH-}H7V9c4cQMiHGwYs4jE`Vg z_^6_7DGSsXMr3zv?PK1he*I_f6v8qIbYtEbki5To-$(!nI%~$*y`!*D-zDv#%7P@+ zWqw(+l+6Suv>)bi;mDYcSh~7TJPv9IdCnAECt!E~m`!8(!{~Zs5W!H20n)#=Xe^sh z;4>9go$jSIRCDfGo73szb9_bq%9_=OI#CW3tS zrjcQcK~kDS(K?!|(^Ksetpvu3CwvyoHo+act4%GxiCEZ#((ymWljwBYali8HSztT7 zb|!0r1ictfzx-0&Kn1-dp4C?rC_&7y``)&S5|c!)6iWUFoLTi*d-!m0!SJD$!5RUJ zs1({Gx-p8Prb>;|7w90k?>mQNJ{j6SZe(efucMjipE(~o$^p%1WVCz7CYX4xq4b@L zgpC~L{59q}h14T+MoS+`Dc zZ!+c#aTOPG=_s{@9#o`E!u&>I%e@2B$YaiULBMAkB#V=S%e6=_#IHk=Kc9sh?W|G_ zqKlD1x6M_nu5}RY>YQ6pM8?!{rh9zk=aGH+RqjuMqhO%W%JuzQ0vwCuoqq8UhiROO zcrj+*g02e||BN+T0l#U+8twjDoE_`9*8X!5-7ER2Ap4{T$ntAq7yrXUhN`H0t|D`4w(SKWi)x^?anxZ zKR_&>pZHZA4=qing>ASASgQ)fNW69kb+wR7E}Akx?UdU+gEJH;(=fil8@Yy}`aUtH zr)t5gD9=E-B{s-WGSbzOAYi+)VycR#oIs_Glud|E4Y0PRZ9lz?hXa9ijEV?6MlzC3 zRD9Wi^8NG<9TV9=Yhh-z*i>&|{A?q%!E6Jd+Yg@V(@9uN{L?GAky+$fr*pL4eG>G? z%4AGJ5j-I<_3Kq&Sz>YCN^2ry|Kwi&z@HKPmUs)E8lBd>?bIJV}y~TPk zFlT}g}e=t6XVEuyZ)YM>T{s2>=A2sD+vm*)?ylZHc;%t)uumwqF^F?iPFcM7ZA?linpZP03eO& z$1zTGz=+LwQ!^GGyQz*F+RxRG4)w*Wn$9eMgs9g~BW5X3y_gR>T)B$Mza(Yt@ca&* zmMVAkRxTqhv29N|ue5=W!PkBZOyJ=4X`!A_Z2}f@ai%_Pau{LhQ^||3=Rq>M^NRh8 z47Kh!+b8y~qkT=LXHNF#gO@giN!0=@V5BZH)b@#low(uEh$qjX3Stx%-a8JAJW6q& z4-%owNH1ek7>Ch3(MeI-FC@QW7g73V1-#^n6F9temp;)WUrhg6LOHW>&O_(A!0pFO zuV@+ri75{}I9fCW`15D~TA$~Dcd|EXKN#XM+oRi`{JztP?(V*HHq&YewDoMUEt?8! zcvIbjj;x@9OA2aZo6W!`&ET=31rB~%&Nt+u6R^?j#}%EOV@P~7{$m%B4xUuW`=&6r z^7C9%xqre2;Xn@xGOi zl#EzF^_J`(_N;!eSM+dbzYYgH<#c*a*aRNqE1&i2*6BryysPC!(~BTH8HJoAbHaF| zKRajY8K~5I@P76=94rFL?w*8Ike%wkrpZsibW+t*`}OCMZjeaNto$e#i+aEmUKmC@ z>Q3#+X`TnOkNA$t&?r#eC%M1x?JC;;iRZki=O{?HKF=OsTL3{h3`V**6-%(FOwmzT zKJK5NUbeM-Xt2mdyyUxV2mjP6m;}i7f zVay5|xRr~3G#Dl|N#dZA-r*mLTR!hXt%`zQ#*tvkLbIaK9B_ObpPQsJh%zEG z%p`^wVA`;;|CK8RN)2evXl1P-g__pS|5|+o)i*>0yiRO_7hOZV@8w8XiK)PZRNFKX z?Pyo%B1{4Q$KPGVN{H}5l(&zUJR5!Ezo7GoRF8IYe@wfbvkK0=B#+V;DcD8x5DUYl z1vKC?d|jTqAE=f(L>ZGfpoJ6v?ux@4m>k~ibC5$nstmu7PE=b2&GUDi&CJ`-rk8oS zi^Vc%BDP*h6y?Oa3x=ee43^PQ*r2URCAX9S6HN>}>xDAYkgx39oz3ci72)7$j&HlvlRbbrI1* q20M!l27vV`eBk2%4yajM>-n|-j|oeKb#>nCLH;`r_`fyxg`)@sAF-faOGGJLN?aX75Otrnly-_y7&`TVn=zMy!B<1R<>C}B zypO)(G>^h@S=+8M2?`>a{TD@0J}}y;!;hHex)Y`lyN}!xv_?JpZ+J)Ge8irimtdmK2hf#IyuhqyEb;0hXKbymI4WMQ2d)d*}GL+}8YvWj>oVuibtU~#8i_cpRl$$&Gl?=F2 zDD5D}9o(@l_2h#aJt?$SuBkWnrtr6&kAJBzg+~?US|a`wjOX@P1qD*rzRaZcQ83o$ z^152_z3A6o|8raRQ{cbP8%Q}wK}zvXL31dDAGNOoK8Im^*JPb}8ctzDo|(BqB%)Bw zhu~-mQI40T15y69+Tq3%l)t?4UH?AH@8lhRJ&5u<>%CSD#8C(p)1G*qKw+X{&;IL4 z6qGW={gaa^?Ejw2Y&%6k)V6lB*cl30$8+=Vq*9n#yua8vgMwwusp|eL3ULi?>f3TC zEOG4DtvyenMxOut{5%SQ^Gho?6i|5Q|G7Z7kixw1>_1UO6x!Lx?uL|5ICrJONE7$n z3@whUU#2j;^r~~x6@2ga#v-`aD7>^gjGraFZ-kbZ+Da`-w zY;e38{U|W7pZ9`7$8d~;V=KPD>q}*-UQ*yY8x*&vox-v!?cTdyQ%IFlGEeNLV4`v? zJL^3Kj~9Qr#`IF?ojP2m+)p9GS4$#mkV4ta(aX<1Q)q0;m#FzdffXsJ>@|Wsu`5{m z0ULS1aoDn*gS_?U)X!8d=D*6OZ3^-BH1iu-*#zHReoHXeq8&T!6?GV)*iLe_?Caqx4^x z$JqCr(MhcPT8A%1Kd=ritK{GKj(+Q3R+;xN1)geWaP=4kf3K#|G(1P-x1Z!yyielQ z+dQTR@6qlhOyklqzTOj}77myP!})OyGS?|=y1YY(!F&(gUuQI! zPT{9R7W*Cg8z|n=@dx82(`T&EhITBv``l_V{^Ii$N}(RlYk2P;f%#Qj_gF9l>q9Oh zNlH1B!n!-n)v1Zd``7k1-Uy-apy)@}c`pj_2c3#{Y^D$`X?KOIi*cAU6cvKJ^}JAh zjrbf2_uKV+zc2&>x^o`!cN3_Q*sPy;pTOgm#8BBH0=uoA{_UGWz(S>2qYU}UY{ARy zzJ(N|^AfIiU?2PXaaoS(CzL;!dsm=|z(9eja7;OYPLp08p$r0}p{(5TBLtQ|jP}gg zMc}xN!!|oB0>95HN*q~B;AvsMQPdIwza&yT%Z1Qh65Sgx!9Zb%7v&pe;EQW8t7(9N z)!B^GtM?3CvJ|V*?`B}iKvVt-;@yjqxko-QV7}p@`a?wmp~)4kiu2HJjo~WKKMV{d z4>c8jWneAs{wl{|AbOvT*!<57+!(s4>OR=`sR4*Rw_& zvh**2>cAjzO6na7%saA`MsN1aA*&rKA1L=Kt#-aRD_D?qo8VPHLR1jIid*CfV=apBLP28jq~inuQ8NvPeyr1_TE9vdc^*{0UCrlr z;SKbsOJ08&Gz=SDmEgD}1LL!n{_yc3ut8Td$FiQl-rvb5WCAG&8|X31_aL8iDmOg| zKtFa{-PU%&{yK4j&98(0N3OMh*hk=@=%xoQl!0YUUoyrTxu6iKYvvyr2}i0ciWl)m2kDpJN^V_mvhXLn+a^UtX#3@8`hz~&y}VE$iIVXf{R5dtXU)CPK1%y z)jtHp-xP0@LqFZ~bOq*Np5w=4zFrikGoD35_@@f48(MciKo~!6cRZ0z(UL-33=jmWAOPv0s{N^M|^Dv z#6DNODT#dH3JVtGvHRIw}e&4fa&?$xBUtkU;JHaQ^kAY+Zbv|1k27ay5D%1~RV3YfSlkrgua85n;ciw{a z;q6(~s7k=wT{0{BfqwatiEv0oP)3iVCTVGwXSU#)M7PwX#s3V4(jpGUCx06rXE>~@ZNGf_Qu481i11K zZ*GbqptbS6cg+C`lS-#N)3kUHF>6@5J+2oH#hf*~AT|PO(xJ}ApV;vClw@VZSq_{{ zK4fFm!vPCztCNo>Il!u~ZQeYK3)un+%dXCFAmy%uh2Rekl*9^(ICD5K93p(g^*sj) z)*thndCmc^;Do<UM~bH> z*%18U>5_!|Z19yiH_DA=!$Jv7g(s$Lc;o(cx8KAF(6_ovyDyJGmZQRCsNM(+7kMMQ+2Fq9lnDE?s#$E7$mSy_Douhf^hteD7Qi`i0!)) YpY6bf=@;LE%vN*ZW^1YITNy6=4>KIs$p8QV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..740c4035b6389d7716757c269b72bea3c963c050 GIT binary patch literal 288 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$720EHL3bhL411?Q|&FqRgS24W$XX67Ya(6Wh?p9{xipm8D`8s?N9nnk`H_P!=7{fypyx9|FEBU zbA>Vcp&#~=YnS|