From d76d8a493c5014043360354d3b1adad75bcf1e5b Mon Sep 17 00:00:00 2001 From: Edoardo Vergerio Date: Wed, 19 Aug 2026 17:50:59 +0200 Subject: [PATCH] repo cleanup --- metaprop_fibermode_doublet_distGrad.py | 692 ------------- metaprop_fibermode_doublet_distGrad_xy.py | 900 ----------------- ..._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 | 798 --------------- metaprop_fibermode_doublet_shiftSource.py | 611 ----------- unit_cell_meep.ipynb | 368 ------- 9 files changed, 6438 deletions(-) delete mode 100644 metaprop_fibermode_doublet_distGrad.py delete mode 100644 metaprop_fibermode_doublet_distGrad_xy.py delete mode 100644 metaprop_fibermode_doublet_distGrad_xy_epigraph.py delete mode 100644 metaprop_fibermode_doublet_epigraph.py delete mode 100644 metaprop_fibermode_doublet_epigraph_ego.py delete mode 100644 metaprop_fibermode_doublet_multi.py delete mode 100644 metaprop_fibermode_doublet_multi_ego.py delete mode 100644 metaprop_fibermode_doublet_shiftSource.py delete mode 100644 unit_cell_meep.ipynb diff --git a/metaprop_fibermode_doublet_distGrad.py b/metaprop_fibermode_doublet_distGrad.py deleted file mode 100644 index c23302b..0000000 --- a/metaprop_fibermode_doublet_distGrad.py +++ /dev/null @@ -1,692 +0,0 @@ -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 deleted file mode 100644 index f7fc09f..0000000 --- a/metaprop_fibermode_doublet_distGrad_xy.py +++ /dev/null @@ -1,900 +0,0 @@ -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 deleted file mode 100644 index 1fba6c7..0000000 --- a/metaprop_fibermode_doublet_distGrad_xy_epigraph.py +++ /dev/null @@ -1,954 +0,0 @@ -''' -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_epigraph.py b/metaprop_fibermode_doublet_epigraph.py deleted file mode 100644 index 06bc55a..0000000 --- a/metaprop_fibermode_doublet_epigraph.py +++ /dev/null @@ -1,659 +0,0 @@ -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_fibermode_doublet_epigraph_ego.py b/metaprop_fibermode_doublet_epigraph_ego.py deleted file mode 100644 index 38170fb..0000000 --- a/metaprop_fibermode_doublet_epigraph_ego.py +++ /dev/null @@ -1,780 +0,0 @@ -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_fibermode_doublet_multi.py b/metaprop_fibermode_doublet_multi.py deleted file mode 100644 index 4bddcc9..0000000 --- a/metaprop_fibermode_doublet_multi.py +++ /dev/null @@ -1,676 +0,0 @@ -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_fibermode_doublet_multi_ego.py b/metaprop_fibermode_doublet_multi_ego.py deleted file mode 100644 index 29e712a..0000000 --- a/metaprop_fibermode_doublet_multi_ego.py +++ /dev/null @@ -1,798 +0,0 @@ -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 = 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 -############################## - -############################## -#Source -beam_waist = 4e-6 -source_spacing = 300e-6 -############################## - -#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 = [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 - -#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 - - 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 = 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__": - run_ego() \ No newline at end of file diff --git a/metaprop_fibermode_doublet_shiftSource.py b/metaprop_fibermode_doublet_shiftSource.py deleted file mode 100644 index 1577e16..0000000 --- a/metaprop_fibermode_doublet_shiftSource.py +++ /dev/null @@ -1,611 +0,0 @@ -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_meep.ipynb b/unit_cell_meep.ipynb deleted file mode 100644 index 2a72e0b..0000000 --- a/unit_cell_meep.ipynb +++ /dev/null @@ -1,368 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 86, - "id": "094bf901", - "metadata": {}, - "outputs": [], - "source": [ - "import meep as mp\n", - "import numpy as np\n", - "import numpy.matlib\n", - "import matplotlib.pyplot as plt\n", - "\n", - "resolution = 120 # pixels/unit length\n", - "\n", - "wvl_min = 1.5\n", - "wvl_max = 1.6\n", - "wvl_cen = 0.5*(wvl_min+wvl_max)\n", - "fmin = 1/wvl_max\n", - "fmax = 1/wvl_min\n", - "fcen = 0.5*(fmin+fmax)\n", - "df = fmax-fmin\n", - "nfreq = 10\n", - "\n", - "dpml = 1.0 #PML thickness\n", - "dsub = 1.5 + wvl_cen #substrate thickness\n", - "dpad = dsub #passind between cell and PML\n", - "\n", - "k_point = mp.Vector3(0,0,0) #used for PBC\n", - "\n", - "n_si = 3.43 #silicon refractive index\n", - "si = mp.Medium(index=n_si)\n", - "n_air = 1.0 #air refractive index\n", - "air = mp.Medium(index=n_air)" - ] - }, - { - "cell_type": "code", - "execution_count": 87, - "id": "35f94716", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " block, center = (0,0,0.35)\n", - " size (0.4,0.6,0.7)\n", - " axes (1,0,0), (0,1,0), (0,0,1)\n", - " block, center = (0,0,-2.025)\n", - " size (1e+20,1e+20,4.05)\n", - " axes (1,0,0), (0,1,0), (0,0,1)\n", - " dielectric constant epsilon diagonal = (11.7649,11.7649,11.7649)\n", - " block, center = (0,0,0.35)\n", - " size (0.4,0.6,0.7)\n", - " axes (1,0,0), (0,1,0), (0,0,1)\n", - " block, center = (0,0,-2.025)\n", - " size (1e+20,1e+20,4.05)\n", - " axes (1,0,0), (0,1,0), (0,0,1)\n", - " dielectric constant epsilon diagonal = (11.7649,11.7649,11.7649)\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "P = 0.7 #unit cell pitch\n", - "h = 0.7 #pillar height\n", - "\n", - "sim_z_size = dpml + dsub + dpad + dpml\n", - "sim_x_size = P\n", - "sim_y_size = P\n", - "\n", - "major_ax = 0.3 #major axis of the ellipse\n", - "minor_ax = 0.2 #minor axis of the ellipse\n", - "\n", - "substrate_geo = mp.Block(\n", - " material = si,\n", - " size = mp.Vector3(mp.inf, mp.inf, dsub+dpml),\n", - " center = mp.Vector3(0,0,-(dsub+dpml)/2),\n", - ")\n", - "\n", - "ellipse_geo = mp.Ellipsoid(\n", - " size = mp.Vector3(major_ax, minor_ax, 2*dpad),\n", - " center = mp.Vector3(0,0,0),\n", - " material = si,\n", - ")\n", - "ellipse_geo.do_averaging = True\n", - "\n", - "cut_box_1 = mp.Block(\n", - " material = air,\n", - " size = mp.Vector3(major_ax, minor_ax, dpad-h),\n", - " center = mp.Vector3(0,0,(dpad-h)/2+h)\n", - ")\n", - "\n", - "cut_box_2 = mp.Block(\n", - " material = air,\n", - " size = mp.Vector3(major_ax, minor_ax, dpad),\n", - " center = mp.Vector3(0,0,-dpad/2)\n", - ")\n", - "\n", - "source_position = mp.Vector3(0,0,-wvl_cen) #source position in the middle of the substrate\n", - "source = mp.Source(\n", - " mp.GaussianSource(fcen, fwidth=df),\n", - " component = mp.Ex,\n", - " center = source_position,\n", - " size = mp.Vector3(sim_x_size, sim_y_size, 0),\n", - ")\n", - "\n", - "cell_size = mp.Vector3(sim_x_size, sim_y_size, sim_z_size)\n", - "pml_layers = [mp.PML(dpml,direction=mp.Z)]\n", - "\n", - "def elliptic_cylinder_func(p):\n", - " # p is an mp.Vector3 point (x, y, z)\n", - " if (p.x / (minor_ax/2)) ** 2 + (p.y / (major_ax/2)) ** 2 <= 1.0 and abs(p.z) <= h and abs(p.z) >= 0:\n", - " return si\n", - " return air\n", - "elliptic_cylinder_func.do_averaging = True\n", - "\n", - "test_ell = mp.Block(\n", - " center=mp.Vector3(0, 0, h/2),\n", - " size=mp.Vector3(2*minor_ax, 2*major_ax, h),\n", - " material=elliptic_cylinder_func,\n", - ")\n", - "\n", - "sim = mp.Simulation(\n", - " resolution = resolution,\n", - " cell_size = cell_size,\n", - " boundary_layers = pml_layers,\n", - " sources = [source],\n", - " #geometry = [ ellipse_geo, cut_box_1, cut_box_2, substrate_geo ],\n", - " geometry = [test_ell, substrate_geo],\n", - " material_function = elliptic_cylinder_func,\n", - " default_material = air,\n", - " k_point = k_point,\n", - ")\n", - "\n", - "monitor_position = mp.Vector3(0,0,h+wvl_cen)\n", - "flux_mon = sim.add_flux(\n", - " fcen, df, nfreq, mp.FluxRegion(center=monitor_position, size=mp.Vector3(sim_x_size, sim_y_size, 0))\n", - ")\n", - "\n", - "f, ax = plt.subplots(1,2,dpi=120)\n", - "yz_plane = mp.Volume(center=mp.Vector3(0,0,0), size=mp.Vector3(0,sim_y_size,sim_z_size))\n", - "xy_plane = mp.Volume(center=mp.Vector3(0,0,h/2), size=mp.Vector3(sim_x_size,sim_y_size,0))\n", - "sim.plot2D(ax=ax[0],output_plane=yz_plane,show_sources=True,show_monitors=True)\n", - "sim.plot2D(ax=ax[1],output_plane=xy_plane,show_sources=True,show_monitors=True,show_epsilon=True)\n", - "ax[1].set_xlim(-sim_x_size/2, sim_x_size/2)\n", - "ax[1].set_ylim(-sim_y_size/2, sim_y_size/2)\n", - "plt.show()\n" - ] - }, - { - "cell_type": "code", - "execution_count": 88, - "id": "e2e8c953", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-----------\n", - "Initializing structure...\n", - "time for choose_chunkdivision = 0.00579405 s\n", - "Working in 3D dimensions.\n", - "Computational cell is 0.7 x 0.7 x 8.1 with resolution 120\n", - " block, center = (0,0,0.35)\n", - " size (0.4,0.6,0.7)\n", - " axes (1,0,0), (0,1,0), (0,0,1)\n", - " block, center = (0,0,-2.025)\n", - " size (1e+20,1e+20,4.05)\n", - " axes (1,0,0), (0,1,0), (0,0,1)\n", - " dielectric constant epsilon diagonal = (11.7649,11.7649,11.7649)\n", - "subpixel-averaging is 8.96239% done, 40.9949 s remaining\n", - "subpixel-averaging is 18.719% done, 17.5123 s remaining\n", - "subpixel-averaging is 28.1353% done, 10.2434 s remaining\n", - "subpixel-averaging is 37.5516% done, 6.67275 s remaining\n", - "subpixel-averaging is 47.4217% done, 4.43799 s remaining\n", - "subpixel-averaging is 57.2918% done, 3.01498 s remaining\n", - "subpixel-averaging is 66.9351% done, 1.99351 s remaining\n", - "subpixel-averaging is 77.1455% done, 1.18899 s remaining\n", - "subpixel-averaging is 87.129% done, 0.594217 s remaining\n", - "subpixel-averaging is 96.8857% done, 0.129903 s remaining\n", - "subpixel-averaging is 3.31345% done, 116.772 s remaining\n", - "subpixel-averaging is 6.60798% done, 56.6936 s remaining\n", - "subpixel-averaging is 9.86465% done, 36.645 s remaining\n", - "subpixel-averaging is 13.1592% done, 26.4271 s remaining\n", - "subpixel-averaging is 16.4727% done, 20.3888 s remaining\n", - "subpixel-averaging is 19.7483% done, 16.3062 s remaining\n", - "subpixel-averaging is 23.0807% done, 13.3563 s remaining\n", - "subpixel-averaging is 26.3373% done, 11.2352 s remaining\n", - "subpixel-averaging is 29.5751% done, 9.54566 s remaining\n", - "subpixel-averaging is 32.9075% done, 8.18746 s remaining\n", - "subpixel-averaging is 35.8044% done, 120.738 s remaining\n", - "subpixel-averaging is 35.8233% done, 109.194 s remaining\n" - ] - }, - { - "ename": "KeyboardInterrupt", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mKeyboardInterrupt\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[87]\u001b[39m\u001b[32m, line 47\u001b[39m, in \u001b[36melliptic_cylinder_func\u001b[39m\u001b[34m(p)\u001b[39m\n\u001b[32m---> \u001b[39m\u001b[32m47\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m elliptic_cylinder_func(p):\n\u001b[32m 48\u001b[39m \u001b[38;5;66;03m# p is an mp.Vector3 point (x, y, z)\u001b[39;00m\n\u001b[32m 49\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m (p.x / (minor_ax/\u001b[32m2\u001b[39m)) ** \u001b[32m2\u001b[39m + (p.y / (major_ax/\u001b[32m2\u001b[39m)) ** \u001b[32m2\u001b[39m <= \u001b[32m1.0\u001b[39m \u001b[38;5;28;01mand\u001b[39;00m abs(p.z) <= h \u001b[38;5;28;01mand\u001b[39;00m abs(p.z) >= \u001b[32m0\u001b[39m:\n\u001b[32m 50\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m si\n", - "\u001b[31mKeyboardInterrupt\u001b[39m: " - ] - }, - { - "ename": "RuntimeError", - "evalue": "meep: Error in typemaps", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mRuntimeError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[88]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m obs_point = monitor_position\n\u001b[32m 2\u001b[39m stop_cond = mp.stop_when_fields_decayed(\u001b[32m25\u001b[39m, mp.Ex, obs_point, \u001b[32m1e-6\u001b[39m)\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m sim.run(until_after_sources=stop_cond)\n\u001b[32m 4\u001b[39m eps_data = sim.get_array(center=mp.Vector3(), size=mp.Vector3(\u001b[32m0\u001b[39m,sim_y_size,sim_z_size), component=mp.Dielectric)\n\u001b[32m 5\u001b[39m ex_data = sim.get_array(center=mp.Vector3(), size=mp.Vector3(\u001b[32m0\u001b[39m,sim_y_size,sim_z_size), component=mp.Ex)\n\u001b[32m 6\u001b[39m plt.figure()\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/mp/lib/python3.13/site-packages/meep/simulation.py:4553\u001b[39m, in \u001b[36mSimulation.run\u001b[39m\u001b[34m(self, *step_funcs, **kwargs)\u001b[39m\n\u001b[32m 4550\u001b[39m until_after_sources = kwargs.pop(\u001b[33m\"\u001b[39m\u001b[33muntil_after_sources\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[32m 4552\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.fields \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m4553\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43minit_sim\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 4555\u001b[39m \u001b[38;5;28mself\u001b[39m._evaluate_dft_objects()\n\u001b[32m 4556\u001b[39m \u001b[38;5;28mself\u001b[39m._check_material_frequencies()\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/mp/lib/python3.13/site-packages/meep/simulation.py:2482\u001b[39m, in \u001b[36mSimulation.init_sim\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 2479\u001b[39m warnings.warn(eps_warning, \u001b[38;5;167;01mRuntimeWarning\u001b[39;00m)\n\u001b[32m 2481\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.structure \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m2482\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_init_structure\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mk_point\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 2484\u001b[39m \u001b[38;5;28mself\u001b[39m.fields = mp.fields(\n\u001b[32m 2485\u001b[39m \u001b[38;5;28mself\u001b[39m.structure,\n\u001b[32m 2486\u001b[39m \u001b[38;5;28mself\u001b[39m.m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.is_cylindrical \u001b[38;5;28;01melse\u001b[39;00m \u001b[32m0\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 2491\u001b[39m \u001b[38;5;28mself\u001b[39m.bfast_scaled_k,\n\u001b[32m 2492\u001b[39m )\n\u001b[32m 2494\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.force_all_components \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mself\u001b[39m.dimensions != \u001b[32m1\u001b[39m:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/mp/lib/python3.13/site-packages/meep/simulation.py:2095\u001b[39m, in \u001b[36mSimulation._init_structure\u001b[39m\u001b[34m(self, k)\u001b[39m\n\u001b[32m 2057\u001b[39m \u001b[38;5;28mself\u001b[39m.gv = gv\n\u001b[32m 2058\u001b[39m \u001b[38;5;28mself\u001b[39m.structure = mp.create_structure(\n\u001b[32m 2059\u001b[39m \u001b[38;5;28mself\u001b[39m.cell_size,\n\u001b[32m 2060\u001b[39m \u001b[38;5;28mself\u001b[39m.dft_data_list,\n\u001b[32m (...)\u001b[39m\u001b[32m 2093\u001b[39m ),\n\u001b[32m 2094\u001b[39m )\n\u001b[32m-> \u001b[39m\u001b[32m2095\u001b[39m \u001b[38;5;28mself\u001b[39m.geps = \u001b[30;43mmp\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_set_materials\u001b[39;49m\u001b[30;43m(\u001b[39;49m\n\u001b[32m 2096\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mstructure\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2097\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mcell_size\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2098\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mgv\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2099\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43meps_averaging\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2100\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43msubpixel_tol\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2101\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43msubpixel_maxeval\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2102\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mgeometry\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2103\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mgeometry_center\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2104\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mensure_periodicity\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43;01mand\u001b[39;49;00m\u001b[30;43m \u001b[39;49m\u001b[30;43;01mnot\u001b[39;49;00m\u001b[30;43m \u001b[39;49m\u001b[30;43;01mnot\u001b[39;49;00m\u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mk_point\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2105\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mdefault_material\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2106\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mabsorbers\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2107\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mextra_materials\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2108\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43msplit_chunks_evenly\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2109\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43;01mTrue\u001b[39;49;00m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2110\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43;01mNone\u001b[39;49;00m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2111\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43;01mFalse\u001b[39;49;00m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2112\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43;01mNone\u001b[39;49;00m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 2113\u001b[39m \u001b[30;43m\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 2115\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._output_stats \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 2116\u001b[39m sys.exit(\u001b[32m0\u001b[39m)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/mp/lib/python3.13/site-packages/meep/__init__.py:4430\u001b[39m, in \u001b[36m_set_materials\u001b[39m\u001b[34m(s, cell_size, gv, use_anisotropic_averaging, tol, maxeval, gobj_list, center, _ensure_periodicity, _default_material, alist, extra_materials, split_chunks_evenly, set_materials, existing_geps, output_chunk_costs, my_bp)\u001b[39m\n\u001b[32m 4429\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_set_materials\u001b[39m(s, cell_size, gv, use_anisotropic_averaging, tol, maxeval, gobj_list, center, _ensure_periodicity, _default_material, alist, extra_materials, split_chunks_evenly, set_materials, existing_geps, output_chunk_costs, my_bp):\n\u001b[32m-> \u001b[39m\u001b[32m4430\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43m_meep\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_set_materials\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43ms\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mcell_size\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mgv\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43muse_anisotropic_averaging\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mtol\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mmaxeval\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mgobj_list\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mcenter\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m_ensure_periodicity\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m_default_material\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43malist\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mextra_materials\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43msplit_chunks_evenly\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mset_materials\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mexisting_geps\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43moutput_chunk_costs\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mmy_bp\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n", - "\u001b[31mRuntimeError\u001b[39m: meep: Error in typemaps" - ] - } - ], - "source": [ - "obs_point = monitor_position\n", - "stop_cond = mp.stop_when_fields_decayed(25, mp.Ex, obs_point, 1e-6)\n", - "sim.run(until_after_sources=stop_cond)\n", - "eps_data = sim.get_array(center=mp.Vector3(), size=mp.Vector3(0,sim_y_size,sim_z_size), component=mp.Dielectric)\n", - "ex_data = sim.get_array(center=mp.Vector3(), size=mp.Vector3(0,sim_y_size,sim_z_size), component=mp.Ex)\n", - "plt.figure()\n", - "plt.imshow(eps_data, interpolation='spline36', cmap='binary')\n", - "plt.imshow(ex_data, interpolation='spline36', cmap='RdBu', alpha=0.9)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "74c01e55", - "metadata": {}, - "outputs": [], - "source": [ - "input_flux = mp.get_fluxes(flux_mon)\n", - "plt.plot(np.array(input_flux)/np.max(input_flux))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "93cee842", - "metadata": {}, - "outputs": [], - "source": [ - "sim.reset_meep()\n", - "\n", - "def make_unit_cell(D):\n", - " pillar_geo = mp.Cylinder(\n", - " material = si,\n", - " radius = D/2,\n", - " height = h,\n", - " center = mp.Vector3(0,0,h/2),\n", - " )\n", - " return pillar_geo\n", - "\n", - "def make_unit_cell_sim(D):\n", - " pillar_geo = make_unit_cell(D)\n", - " geometries = [substrate_geo, pillar_geo]\n", - "\n", - " sim = mp.Simulation(\n", - " resolution = resolution,\n", - " cell_size = cell_size,\n", - " boundary_layers = pml_layers,\n", - " geometry = geometries,\n", - " default_material = air,\n", - " k_point = k_point,\n", - " sources = [source],\n", - " )\n", - " f = plt.figure(dpi=120)\n", - " sim.plot2D(ax=f.gca(),output_plane=yz_plane,show_sources=True,show_monitors=True)\n", - " plt.show()\n", - "\n", - " mode_mon = sim.add_flux(\n", - " fcen, df, nfreq, mp.FluxRegion(center=monitor_position, size=mp.Vector3(sim_x_size, sim_y_size, 0))\n", - " )\n", - " sim.run(until_after_sources=300)\n", - "\n", - " freqs = mp.get_eigenmode_freqs(mode_mon)\n", - " res = sim.get_eigenmode_coefficients(\n", - " mode_mon, [1], eig_parity=mp.EVEN_Y\n", - " )\n", - " coeffs = res.alpha\n", - "\n", - " mode_wvl = [1/freqs[nf] for nf in range(nfreq)]\n", - " mode_tran = [abs(coeffs[0,nf,0])**2 / input_flux[nf] for nf in range(nfreq)]\n", - " mode_phase = [np.angle(coeffs[0,nf,0]) for nf in range(nfreq)]\n", - "\n", - " return mode_wvl, mode_tran, mode_phase\n", - "\n", - "diameters = np.linspace(0.1, 0.7, 4)\n", - "mode_tran_arr = np.empty((len(diameters), nfreq))\n", - "mode_phase_arr = np.empty((len(diameters), nfreq))\n", - "\n", - "for i, D in enumerate(diameters):\n", - " mode_wvl, mode_tran, mode_phase = make_unit_cell_sim(D)\n", - " mode_tran_arr[i,:] = mode_tran\n", - " mode_phase_arr[i,:] = mode_phase\n", - "\n", - "#make a 2d plot of the transmission and phase as a function of wavelength and diameter\n", - "plt.figure(dpi=120)\n", - "plt.subplot(2,1,1)\n", - "plt.imshow(mode_tran_arr, extent=[wvl_min*1e6, wvl_max*1e6, diameters[0], diameters[-1]], aspect='auto', origin='lower', cmap='viridis')\n", - "plt.colorbar(label='Transmission')\n", - "plt.ylabel('Diameter (um)')\n", - "plt.title('Transmission vs Wavelength and Diameter')\n", - "plt.subplot(2,1,2)\n", - "plt.imshow(mode_phase_arr, extent=[wvl_min*1e6, wvl_max*1e6, diameters[0], diameters[-1]], aspect='auto', origin='lower', cmap='twilight')\n", - "plt.colorbar(label='Phase (radians)')\n", - "plt.ylabel('Diameter (um)')\n", - "plt.xlabel('Wavelength (um)')\n", - "plt.title('Phase vs Wavelength and Diameter')\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "352d606d", - "metadata": {}, - "outputs": [], - "source": [ - "print(input_flux[0])" - ] - } - ], - "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 -}