repository update. Added metaprop_fibermode_doublet_sym.py which implements a constrained optimization enforcing mirror symmetry about the x and y axes. Mirror symmetry can be exploited in this problem since the target mode has such symmetry. Each metasurface has effectively a quarter of the degrees of freedom compared to the unconstrained problem.

This commit is contained in:
Edoardo Vergerio
2026-08-19 17:48:11 +02:00
parent 5ca4f4045a
commit e63c11ea58
37 changed files with 11478 additions and 40 deletions
+48 -40
View File
@@ -26,19 +26,23 @@ if not os.path.exists("results"):
##############################
#physics and simulation domain
wavelength = 1.55e-6
n0 = 1.0
n0 = 1.45
n1 = 1.0
n2 = 1.0
n2 = 1.45
k0 = 2 * np.pi / wavelength
k1 = k0
k2 = k0
lda0 = wavelength / n0
lda1 = wavelength / n1
lda2 = wavelength / n2
opt_max_eval = 250
k0 = 2 * np.pi / lda0
k1 = 2 * np.pi / lda1
k2 = 2 * np.pi / lda2
opt_max_eval = 150
unit_cell_pitch = 700e-9 #equivalent to spatial sampling
Nx = 2048 #pixels per dimension
Nx = 1024 #pixels per dimension
Ny = Nx
S = Nx * Ny
@@ -53,10 +57,9 @@ rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d0 = 300e-6 #propagation distance: source - MS1
d1 = 500e-6 #propagation distance: MS1 - MS2
d2 = 400e-6 #propagation distance: MS2 - target
d = [d1, d2] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target
d0 = 403e-6 #propagation distance: source - MS1
d1 = 158e-6 #propagation distance: MS1 - MS2
d2 = d0 #propagation distance: MS2 - target
phase_min = 0
phase_max = 2 * np.pi
@@ -88,13 +91,13 @@ KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = k0 * d0 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
phase_factor_0 = k0 * d0 * np.sqrt(1 - (lda0 * nu_parallel) ** 2 + 0*1j)
P_0 = np.exp(1j * phase_factor_0)
P_0_nat = np.fft.ifftshift(P_0)
phase_factor_1 = k0 * d1 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
phase_factor_1 = k1 * d1 * np.sqrt(1 - (lda1 * nu_parallel) ** 2 + 0*1j)
P_1 = np.exp(1j * phase_factor_1)
P_1_nat = np.fft.ifftshift(P_1)
phase_factor_2 = k0 * d2 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
phase_factor_2 = k2 * d2 * np.sqrt(1 - (lda2 * nu_parallel) ** 2 + 0*1j)
P_2 = np.exp(1j * phase_factor_2)
P_2_nat = np.fft.ifftshift(P_2)
@@ -106,7 +109,6 @@ P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj()
##############################
#Plot zoom parameters [um]
#Plot zoom parameters [um]
zoom_x = 150
zoom_y = 150
zoom_output_fiber_radius = 63
@@ -146,7 +148,7 @@ def get_fiber_mode_pattern(mode_list:list)->list:
for enum_index, n in enumerate(mode_list):
#try to load mode index n
try:
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy"
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
pattern = np.load(pattern_file)
output_patterns.append(pattern)
print("Loaded fiber patterns for mode index: ", n)
@@ -209,7 +211,7 @@ def get_fiber_mode_pattern(mode_list:list)->list:
else:#outside core
E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi)
#save the computed pattern to a file for future use
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy"
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
np.save(pattern_file, E_field)
output_patterns.append(E_field)
return output_patterns
@@ -366,6 +368,8 @@ make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field
#initialize input field (propagated source field)
input_field = propagate_source(source_field)
input_field_2d = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#save the input field to numpy array
np.save("results/input_field.npy", input_field_2d)
#input field integrated intensity
input_field_intensity = np.sum(np.abs(input_field)**2)
print("Input field integrated intensity: %.4e" % input_field_intensity)
@@ -376,7 +380,7 @@ make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_p
###########################
#initialize target field
target_mode_indices = [1]
target_mode_indices = [2]
fiber_mode_pattern = get_fiber_mode_pattern(target_mode_indices)
target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2))
target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1)
@@ -416,7 +420,7 @@ def forward_propagate() -> None:
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))
fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
@@ -434,14 +438,14 @@ def forward_propagate() -> None:
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields[0] = ifft_operator.output_array.copy()
intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy())
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2))
fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:].reshape((Nx, Ny))))
fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields[1] = ifft_operator.output_array.copy()
intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy())
end_time = time.time()
#make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
@@ -462,19 +466,19 @@ def adjoint_propagate():
Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d
fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d)
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger
fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger)
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * fft_input_array).flatten()
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
@@ -494,6 +498,7 @@ def _compute_cost():
return C_t
opt_history = []
iter_num = [0]
def cost_fun(x, grad):
''' Cost function to be minimized.
Args:
@@ -509,15 +514,16 @@ def cost_fun(x, grad):
C = _compute_cost()
opt_history.append(C)
plt.figure()
plt.plot(opt_history)
plt.xlabel('Iteration')
plt.ylabel('Cost Function Value')
plt.title('Optimization History')
plt.tight_layout()
plt.savefig("results/modeconv1_optimization_history.pdf")
plt.close()
iter_num[0] += 1
if iter_num[0] % 10 == 0:
plt.figure()
plt.plot(opt_history)
plt.xlabel('Iteration')
plt.ylabel('Cost Function Value')
plt.title('Optimization History')
plt.tight_layout()
plt.savefig("results/optimization_history.pdf")
plt.close()
if grad.size > 0:
grad[:] = adjoint_propagate() * 2 * np.pi
@@ -539,10 +545,11 @@ if __name__ == "__main__":
if start:
w_norm[:] = solver.optimize(w_norm)
print("Optimization completed.")
print("LAST OPTIMUM VALUE: ", solver.last_optimum_value())
#verify the results
phase_mask = phase_given_w(w_norm)
forward_propagate()
field_input_ms2 = intermediate_fields[0]
output_field = intermediate_fields[-1]
input_power = np.sum(np.abs(input_field)**2)
@@ -552,9 +559,10 @@ if __name__ == "__main__":
print("Norm. output power: %.4e" % output_power)
#save the output field for later use
save_output_field = False
save_output_field = True
if save_output_field:
np.savetxt("results/optimized_output_field.txt", output_field.reshape((Nx, Ny)))
np.save("results/intermediate_field_0.npy", intermediate_fields[0].reshape((Nx, Ny)))
np.save("results/optimized_output_field.npy", output_field.reshape((Nx, Ny)))
# Plot results in individual PDF figures
# Reshape fields to 2D for visualization
@@ -580,7 +588,7 @@ if __name__ == "__main__":
#save the phase masks for later use
savePhase_masks = True
if savePhase_masks:
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2)
np.save("results/optimized_phase_mask_1.npy", phase_mask_1)
np.save("results/optimized_phase_mask_2.npy", phase_mask_2)
print("All plots saved to results/ folder.")
+692
View File
@@ -0,0 +1,692 @@
import time
import multiprocessing
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
import os
if not os.path.exists("results"):
os.makedirs("results")
##############################
#physics and simulation domain
wavelength = 1.55e-6
n0 = 1.0
n1 = 1.0
n2 = 1.0
lda0 = wavelength / n0
lda1 = wavelength / n1
lda2 = wavelength / n2
k0 = 2 * np.pi / lda0
k1 = 2 * np.pi / lda1
k2 = 2 * np.pi / lda2
opt_max_eval = 250
unit_cell_pitch = 700e-9 #equivalent to spatial sampling
Nx = 2048 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d0_init = 300e-6 #propagation distance: source - MS1
d1_init = 500e-6 #propagation distance: MS1 - MS2
d2_init = 400e-6 #propagation distance: MS2 - target
distance_num = 3
d = np.array([d0_init, d1_init, d2_init])
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#Source
beam_waist = 5.2e-6
#output fibre
output_fiber_core_diameter = 19e-6
##############################
##############################
#plan pyfftw objects for forward and inverse FFTs
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = []
P_0 = []
P_0_nat = []
phase_factor_1 = []
P_1 = []
P_1_nat = []
phase_factor_2 = []
P_2 = []
P_2_nat = []
P_1_dagger = []
P_2_dagger = []
P_1_dagger_nat = []
P_2_dagger_nat = []
##############################
def compute_phase_factors():
phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0[:] = np.exp(1j * phase_factor_0)
P_0_nat[:] = np.fft.ifftshift(P_0)
phase_factor_1[:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1[:] = np.exp(1j * phase_factor_1)
P_1_nat[:] = np.fft.ifftshift(P_1)
phase_factor_2[:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2[:] = np.exp(1j * phase_factor_2)
P_2_nat[:] = np.fft.ifftshift(P_2)
P_1_dagger[:] = P_1.T.conj()
P_2_dagger[:] = P_2.T.conj()
P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj()
##############################
#Plot zoom parameters [um]
#Plot zoom parameters [um]
zoom_x = 150
zoom_y = 150
zoom_output_fiber_radius = 63
full_view_x = size_x * 1e6 / 2
full_view_y = size_y * 1e6 / 2
##############################
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern(mode_list:list)->list:
'''Returns the fiber mode pattern for the selected n index.
Args:
mode_list: the list of the selected fiber mode indices
Returns:
The fiber mode patterns corresponding to the selected indices returned
as a list of 2D arrays with the same order as the input mode_list
'''
#check if the modes in mode_list have already been compute by checking
#the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files.
fiber_modes_input_folder = "fiber_modes"
output_patterns = []
try:
for enum_index, n in enumerate(mode_list):
#try to load mode index n
try:
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy"
pattern = np.load(pattern_file)
output_patterns.append(pattern)
print("Loaded fiber patterns for mode index: ", n)
#if mode index n was not found, compute it
except FileNotFoundError:
if not os.path.exists(fiber_modes_input_folder):
os.makedirs(fiber_modes_input_folder)
print("Computing fiber patterns for mode index: ", n)
lda = 1.55e-6
n_co = 1.4630 #core refractive index
n_cl = 1.4585 #cladding refractive index
fiber_core_diameter = output_fiber_core_diameter # [m]
a = fiber_core_diameter / 2 #fiber core radius
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
b_solutions_at_V_obs = []
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
b_mode_sel = b_solutions_at_V_obs[0]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi)
else:#outside core
E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi)
#save the computed pattern to a file for future use
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy"
np.save(pattern_file, E_field)
output_patterns.append(E_field)
return output_patterns
except:
raise(Exception("Error in computing fiber patterns"))
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the simulation spatial grid by a given physical (true space) amount.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X + shift_amount_x
Yp = Y + shift_amount_y
return Xp, Yp
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates.
Normalization consists in scaling the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The 2D normalized Gaussian source field.
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source(source_to_propagate):
'''Returns the propagated source field (flattened) just before metasurface 1 (MS1).
This involves propagating the source for a distance d0: from its plane of definition up to MS1.
Args:
source_to_propagate: the source field to be propagated (flattened)
Returns:
The propagated source field (flattened) just before MS1.
'''
fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny)))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
plt.plot(x_axis[i], y_axis[i])
plt.xlabel("x [um]")
plt.tight_layout()
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None:
'''Makes a polar plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_r: zoom level for the radial axis (in micrometers)
Returns:
None
'''
fig, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting
im = ax.imshow(given_field_2d,origin='lower')
elif choose_quantity == "phase":
given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting
im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
center_x, center_y = Nx//2, Ny//2
radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels
circle = patches.Circle((center_x, center_y), radius, transform=ax.transData)
im.set_clip_path(circle)
ax.set_xlim(center_x - radius, center_x + radius)
ax.set_ylim(center_y - radius, center_y + radius)
ax.axis('off')
cbar = plt.colorbar(im, ax=ax)
plt.savefig("results/" + save_name + ".pdf")
###########################
#initialize source field
source_field = get_source_field(beam_waist)
source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#source field integrated intensity
source_power = np.sum(np.abs(source_field)**2)
print("Source field integrated intensity: %.4e" % source_power)
#make a plot and save the source field
make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
###########################
###########################
#initialize input field (propagated source field)
input_field = []
input_field_2d = []
#make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
def compute_input_field():
input_field[:] = propagate_source(source_field)
input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field integrated intensity
input_field_intensity = np.sum(np.abs(input_field)**2)
print("Input field integrated intensity: %.4e" % input_field_intensity)
###########################
###########################
#initialize target field
target_mode_indices = [1]
fiber_mode_pattern = get_fiber_mode_pattern(target_mode_indices)
target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2))
target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1)
# Plot: Target field
make_polarPlot_of(target_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
###########################
###########################
#initialize the normalized phase masks and global phase mask
w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks
X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny))
rho = np.sqrt(X**2 + Y**2)
circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0)
#w_norm[:S] += circular_mask_1
target_pattern_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[S:] += target_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
phase_mask = phase_given_w(w_norm)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
norm_phase_min = 0
norm_phase_max = 1
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
###########################
intermediate_fields = [None, None]
def forward_propagate() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields[0] = ifft_operator.output_array.copy()
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2))
fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields[1] = ifft_operator.output_array.copy()
end_time = time.time()
#make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def adjoint_propagate():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * fft_input_array).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def dist_grad():
P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2)
P_0_triangle = P_0 * P_0_tilde
P_0_triangle_nat = np.fft.ifftshift(P_0_triangle)
P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2)
P_1_triangle = P_1 * P_1_tilde
P_1_triangle_nat = np.fft.ifftshift(P_1_triangle)
P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2)
P_2_triangle = P_2 * P_2_tilde
P_2_triangle_nat = np.fft.ifftshift(P_2_triangle)
Phi_1 = np.exp(1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2 = np.exp(1j * phase_mask[S:]).reshape((Nx, Ny))
S = np.sum(intermediate_fields[1] * np.conj(target_Efield_2d))
S_dagger = np.conj(S)
E_t_dagger = np.conj(target_Efield_2d)
################################
#dC_d0
fft_input_array[:,:] = input_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d1
fft_input_array[:,:] = input_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d2
fft_input_array[:,:] = input_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat
ifft_operator()
dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
return np.concatenate((dC_d0, dC_d1, dC_d2))
def _compute_cost():
forward_propagate()
output_field = intermediate_fields[-1].flatten()
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field * np.conj(target_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
opt_history = []
def cost_fun(x, grad):
''' Cost function to be minimized.
Args:
x: input normalized parameter field (flattened)
grad: gradient of the cost function with respect to x, modified in place (flattened)
Returns:
The cost function value for the input parameter field x.
'''
d[:] = x[:distance_num] #update distance variables
w_norm[:] = x[distance_num:] #update normalized phase mask parameters
phase_mask[:] = phase_given_w(w_norm)
compute_phase_factors()
compute_input_field()
C = _compute_cost()
opt_history.append(C)
plt.figure()
plt.plot(opt_history)
plt.xlabel('Iteration')
plt.ylabel('Cost Function Value')
plt.title('Optimization History')
plt.tight_layout()
plt.savefig("results/modeconv1_optimization_history.pdf")
plt.close()
if grad.size > 0:
grad[:] = adjoint_propagate() * 2 * np.pi
return C
if __name__ == "__main__":
#initialize nlopt solver
solver = nlopt.opt(nlopt.LD_CCSAQ, distance_num + 2*S)
solver.set_lower_bounds(mask_lower_bounds.flatten())
solver.set_upper_bounds(mask_upper_bounds.flatten())
solver.set_max_objective(cost_fun)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-7)
solver.set_param("verbosity",1)
print("Starting optimization...")
start = True
dist_and_w_norm = np.concatenate((d, w_norm), axis=0) #initialize optimization variables
if start:
dist_and_w_norm[:] = solver.optimize(dist_and_w_norm)
print("Optimization completed.")
d[:] = dist_and_w_norm[:distance_num] #update distance variables
w_norm[:] = dist_and_w_norm[distance_num:] #update normalized phase mask parameters
#verify the results
compute_phase_factors()
compute_input_field()
phase_mask = phase_given_w(w_norm)
forward_propagate()
output_field = intermediate_fields[-1]
input_power = np.sum(np.abs(input_field)**2)
output_power = np.sum(np.abs(output_field)**2)
print("Norm. input power: %.4e" % input_power)
print("Norm. output power: %.4e" % output_power)
#save the output field for later use
save_output_field = False
if save_output_field:
np.savetxt("results/optimized_output_field.txt", output_field.reshape((Nx, Ny)))
# Plot results in individual PDF figures
# Reshape fields to 2D for visualization
output_field_2d = output_field.reshape((Nx, Ny))
phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(w_norm[S:]).reshape((Nx, Ny))
phase_mask_zoom = 150
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
# Plot: Output field amplitude and phase
make_polarPlot_of(output_field_2d, choose_quantity="amplitude", save_name="optimized_output_field_amplitude", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field_2d, choose_quantity="phase", save_name="optimized_output_field_phase", plot_zoom_r=zoom_output_fiber_radius)
slice_y_index = Ny//2
slice_x = xs * 1e6
slice_target = target_Efield.reshape((Nx, Ny))[slice_y_index,:]
slice_output = output_field.reshape((Nx, Ny))[slice_y_index,:]
make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude")
make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase")
#save the phase masks for later use
savePhase_masks = True
if savePhase_masks:
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2)
print("All plots saved to results/ folder.")
+900
View File
@@ -0,0 +1,900 @@
import gc
import time
import multiprocessing
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
import os
if not os.path.exists("results"):
os.makedirs("results")
##############################
#physics and simulation domain
wavelength = 1.55e-6
n0 = 1.0
n1 = 1.0
n2 = 1.0
lda0 = wavelength / n0
lda1 = wavelength / n1
lda2 = wavelength / n2
k0 = 2 * np.pi / lda0
k1 = 2 * np.pi / lda1
k2 = 2 * np.pi / lda2
opt_max_eval = 150
unit_cell_pitch = 700e-9 #equivalent to spatial sampling
Nx = 2048 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d0_init = 300e-6 #propagation distance: source - MS1
d1_init = 500e-6 #propagation distance: MS1 - MS2
d2_init = 400e-6 #propagation distance: MS2 - target
distance_lower_bound = 100e-6
distance_upper_bound = 1000e-6
distance_num = 3
d = np.array([d0_init, d1_init, d2_init])
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#Source
beam_waist = 5.2e-6
#output fibre
output_fiber_core_diameter = 19e-6
##############################
##############################
#plan pyfftw objects for forward and inverse FFTs
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = np.zeros((Nx, Ny), dtype=complex)
P_0 = np.zeros((Nx, Ny), dtype=complex)
P_0_nat = np.zeros((Nx, Ny), dtype=complex)
phase_factor_1 = np.zeros((Nx, Ny), dtype=complex)
P_1 = np.zeros((Nx, Ny), dtype=complex)
P_1_nat = np.zeros((Nx, Ny), dtype=complex)
phase_factor_2 = np.zeros((Nx, Ny), dtype=complex)
P_2 = np.zeros((Nx, Ny), dtype=complex)
P_2_nat = np.zeros((Nx, Ny), dtype=complex)
P_1_dagger = np.zeros((Nx, Ny), dtype=complex)
P_2_dagger = np.zeros((Nx, Ny), dtype=complex)
P_1_dagger_nat = np.zeros((Nx, Ny), dtype=complex)
P_2_dagger_nat = np.zeros((Nx, Ny), dtype=complex)
##############################
def compute_phase_factors():
phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0[:] = np.exp(1j * phase_factor_0)
P_0_nat[:] = np.fft.ifftshift(P_0)
phase_factor_1[:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1[:] = np.exp(1j * phase_factor_1)
P_1_nat[:] = np.fft.ifftshift(P_1)
phase_factor_2[:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2[:] = np.exp(1j * phase_factor_2)
P_2_nat[:] = np.fft.ifftshift(P_2)
P_1_dagger[:] = P_1.T.conj()
P_2_dagger[:] = P_2.T.conj()
P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj()
##############################
#Plot zoom parameters [um]
#Plot zoom parameters [um]
zoom_x = 150
zoom_y = 150
zoom_output_fiber_radius = 63
full_view_x = size_x * 1e6 / 2
full_view_y = size_y * 1e6 / 2
##############################
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern(mode_list:list)->list:
'''Returns the fiber mode pattern for the selected n index.
Args:
mode_list: the list of the selected fiber mode indices
Returns:
The fiber mode patterns corresponding to the selected indices returned
as a list of 2D arrays with the same order as the input mode_list
'''
#check if the modes in mode_list have already been compute by checking
#the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files.
fiber_modes_input_folder = "fiber_modes"
output_patterns = []
try:
for enum_index, n in enumerate(mode_list):
#try to load mode index n
try:
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy"
pattern = np.load(pattern_file)
output_patterns.append(pattern)
print("Loaded fiber patterns for mode index: ", n)
#if mode index n was not found, compute it
except FileNotFoundError:
if not os.path.exists(fiber_modes_input_folder):
os.makedirs(fiber_modes_input_folder)
print("Computing fiber patterns for mode index: ", n)
lda = 1.55e-6
n_co = 1.4630 #core refractive index
n_cl = 1.4585 #cladding refractive index
fiber_core_diameter = output_fiber_core_diameter # [m]
a = fiber_core_diameter / 2 #fiber core radius
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
b_solutions_at_V_obs = []
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
b_mode_sel = b_solutions_at_V_obs[0]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi)
else:#outside core
E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi)
#save the computed pattern to a file for future use
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy"
np.save(pattern_file, E_field)
output_patterns.append(E_field)
return output_patterns
except:
raise(Exception("Error in computing fiber patterns"))
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the simulation spatial grid by a given physical (true space) amount.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X + shift_amount_x
Yp = Y + shift_amount_y
return Xp, Yp
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates.
Normalization consists in scaling the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The 2D normalized Gaussian source field.
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source(source_to_propagate):
'''Returns the propagated source field (flattened) just before metasurface 1 (MS1).
This involves propagating the source for a distance d0: from its plane of definition up to MS1.
Args:
source_to_propagate: the source field to be propagated (flattened)
Returns:
The propagated source field (flattened) just before MS1.
'''
fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny)))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
plt.plot(x_axis[i], y_axis[i])
plt.xlabel("x [um]")
plt.tight_layout()
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None:
'''Makes a polar plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_r: zoom level for the radial axis (in micrometers)
Returns:
None
'''
fig, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting
im = ax.imshow(given_field_2d,origin='lower')
elif choose_quantity == "phase":
given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting
im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
center_x, center_y = Nx//2, Ny//2
radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels
circle = patches.Circle((center_x, center_y), radius, transform=ax.transData)
im.set_clip_path(circle)
ax.set_xlim(center_x - radius, center_x + radius)
ax.set_ylim(center_y - radius, center_y + radius)
ax.axis('off')
cbar = plt.colorbar(im, ax=ax)
plt.savefig("results/" + save_name + ".pdf")
###########################
#initialize source field
source_field = get_source_field(beam_waist)
source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#source field integrated intensity
source_power = np.sum(np.abs(source_field)**2)
print("Source field integrated intensity: %.4e" % source_power)
#make a plot and save the source field
make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
###########################
###########################
#initialize input field (propagated source field)
input_field = np.zeros((S,), dtype=complex)
input_field_2d = np.zeros((Nx, Ny), dtype=complex)
#make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
def compute_input_field():
input_field[:] = propagate_source(source_field)
input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field integrated intensity
input_field_intensity = np.sum(np.abs(input_field)**2)
print("Input field integrated intensity: %.4e" % input_field_intensity)
###########################
###########################
#initialize target field
target_mode_indices = [1,2]
fiber_mode_pattern1, fiber_mode_pattern2 = get_fiber_mode_pattern(target_mode_indices)
target1_Efield_2d = fiber_mode_pattern1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern1)**2))
target2_Efield_2d = fiber_mode_pattern2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern2)**2))
target1_Efield = target1_Efield_2d.flatten() #normalized target field (intensity = 1)
target2_Efield = target2_Efield_2d.flatten() #normalized target field (intensity = 1)
# Plot: Target field
make_polarPlot_of(target1_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target1_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target2_Efield_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target2_Efield_2d, choose_quantity="phase", save_name="target_field_2_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
###########################
###########################
#initialize the normalized phase masks and global phase mask
w_norm = np.zeros((4*S)) #concatenated normalized parameters for both masks
X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny))
rho = np.sqrt(X**2 + Y**2)
circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0)
#w_norm[:S] += circular_mask_1
target1_pattern_phase = (np.angle(target1_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[S:2*S] += target1_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
target2_pattern_phase = (np.angle(target2_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[3*S:4*S] += target2_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
phase_mask = phase_given_w(w_norm)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
norm_phase_min = 0
norm_phase_max = 1
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
distance_upper_bounds = np.array([1, 1, 1])
distance_lower_bounds = np.array([0, 0, 0])
distance_and_mask_upper_bounds = np.concatenate((distance_upper_bounds, mask_upper_bounds, mask_upper_bounds),axis=0)
distance_and_mask_lower_bounds = np.concatenate((distance_lower_bounds, mask_lower_bounds, mask_lower_bounds),axis=0)
###########################
intermediate_fields_1 = [None, None]
intermediate_fields_2 = [None, None]
def forward_propagate_1() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields_1[0] = ifft_operator.output_array.copy()
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_1[0])**2))
fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:2*S].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields_1[1] = ifft_operator.output_array.copy()
end_time = time.time()
make_2Dplot_of(intermediate_fields_1[0], choose_quantity="amplitude", save_name="intermediate_field_1_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(intermediate_fields_1[1], choose_quantity="amplitude", save_name="intermediate_field_1_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def forward_propagate_2() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[2*S:3*S])).reshape((Nx, Ny))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields_2[0] = ifft_operator.output_array.copy()
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_2[0])**2))
fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[3*S:4*S].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields_2[1] = ifft_operator.output_array.copy()
end_time = time.time()
#make_2Dplot_of(intermediate_fields_2[0], choose_quantity="amplitude", save_name="intermediate_field_2_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(intermediate_fields_2[1], choose_quantity="amplitude", save_name="intermediate_field_2_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def adjoint_propagate_1():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:2*S]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d)) * target1_Efield_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_1[0].conj() * fft_input_array).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def adjoint_propagate_2():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[2*S:3*S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[3*S:4*S]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d)) * target2_Efield_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_2[0].conj() * fft_input_array).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def dist_grad_1():
P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j)
print("shape of P_0_tilde: ", P_0_tilde.shape)
P_0_triangle = P_0 * P_0_tilde
P_0_triangle_nat = np.fft.ifftshift(P_0_triangle)
P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j)
P_1_triangle = P_1 * P_1_tilde
P_1_triangle_nat = np.fft.ifftshift(P_1_triangle)
P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j)
P_2_triangle = P_2 * P_2_tilde
P_2_triangle_nat = np.fft.ifftshift(P_2_triangle)
Phi_1 = np.exp(1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2 = np.exp(1j * phase_mask[S:2*S]).reshape((Nx, Ny))
Sf = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d))
S_dagger = np.conj(Sf)
E_t_dagger = np.conj(target1_Efield_2d)
################################
#dC_d0
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d1
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d2
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat
ifft_operator()
dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
return np.array([dC_d0, dC_d1, dC_d2])
def dist_grad_2():
P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j)
print("shape of P_0_tilde: ", P_0_tilde.shape)
P_0_triangle = P_0 * P_0_tilde
P_0_triangle_nat = np.fft.ifftshift(P_0_triangle)
P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j)
P_1_triangle = P_1 * P_1_tilde
P_1_triangle_nat = np.fft.ifftshift(P_1_triangle)
P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j)
P_2_triangle = P_2 * P_2_tilde
P_2_triangle_nat = np.fft.ifftshift(P_2_triangle)
Phi_1 = np.exp(1j * phase_mask[2*S:3*S]).reshape((Nx, Ny))
Phi_2 = np.exp(1j * phase_mask[3*S:4*S]).reshape((Nx, Ny))
Sf = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d))
S_dagger = np.conj(Sf)
E_t_dagger = np.conj(target2_Efield_2d)
################################
#dC_d0
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d1
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d2
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat
ifft_operator()
dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
return np.array([dC_d0, dC_d1, dC_d2])
def dist_norm_to_real(norm_dist):
'''Maps normalized distance parameters to real distance values.
Args:
norm_dist: normalized distance parameters (flattened)
Returns:
Real distance values corresponding to the normalized parameters (flattened)'''
return norm_dist * (distance_upper_bound - distance_lower_bound) + distance_lower_bound
def _compute_cost_1():
forward_propagate_1()
output_field_1 = intermediate_fields_1[-1].flatten()
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field_1 * np.conj(target1_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
def _compute_cost_2():
forward_propagate_2()
output_field_2 = intermediate_fields_2[-1].flatten()
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field_2 * np.conj(target2_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
opt_history_1 = []
opt_history_2 = []
opt_cycle = [0]
def cost_fun(x, grad):
''' Cost function to be minimized.
Args:
x: input normalized parameter field (flattened)
grad: gradient of the cost function with respect to x, modified in place (flattened)
Returns:
The cost function value for the input parameter field x.
'''
d[:] = dist_norm_to_real(x[:distance_num]) #update distance variables
print("Current distances: ", d)
w_norm[:] = x[distance_num:] #update normalized phase mask parameters
phase_mask[:] = phase_given_w(w_norm)
compute_phase_factors() #update phase factor matrices
compute_input_field() #update input field based on the new distances
C1 = _compute_cost_1()
C2 = _compute_cost_2()
C = np.mean([C1, C2]) #average cost function for both modes
opt_history_1.append(C1)
opt_history_2.append(C2)
if opt_cycle[0] % 10 == 0:
plt.figure()
plt.plot(opt_history_1, 'r', label="Mode #1", markersize=2)
plt.plot(opt_history_2, 'b', label="Mode #2", markersize=2)
plt.xlabel('Iteration')
plt.ylabel('Cost Function Value')
plt.title('Optimization History')
plt.tight_layout()
plt.legend()
plt.savefig("results/modeconv1_optimization_history.pdf")
plt.close()
opt_cycle[0] = opt_cycle[0] + 1
if grad.size > 0:
if opt_cycle[0] < 1000:
grad[:distance_num] = np.zeros(distance_num) #set distance gradient to zero for the first 100 iterations
else:
grad[:distance_num] = dist_grad_1() * (distance_upper_bound - distance_lower_bound) + dist_grad_2() * (distance_upper_bound - distance_lower_bound)
#grad[:distance_num] = np.array([0,0,0]) #set distance gradient to zero for now
grad[distance_num:2*S+distance_num] = adjoint_propagate_1() * 2 * np.pi * 0.5
grad[2*S+distance_num:4*S+distance_num] = adjoint_propagate_2() * 2 * np.pi * 0.5
gc.collect() #force garbage collection to free memory
return C
if __name__ == "__main__":
#initialize nlopt solver
solver = nlopt.opt(nlopt.LD_CCSAQ, distance_num + 4*S)
solver.set_lower_bounds(distance_and_mask_lower_bounds.flatten())
solver.set_upper_bounds(distance_and_mask_upper_bounds.flatten())
solver.set_max_objective(cost_fun)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-7)
solver.set_param("verbosity",1)
print("Starting optimization...")
start = True
initial_d = np.array([0.4, 0.5, 0.3])
dist_and_w_norm = np.concatenate((initial_d, w_norm), axis=0) #initialize optimization variables
if start:
dist_and_w_norm[:] = solver.optimize(dist_and_w_norm)
print("Optimization completed.")
d[:] = dist_norm_to_real(dist_and_w_norm[:distance_num]) #update distance variables
w_norm[:] = dist_and_w_norm[distance_num:] #update normalized phase mask parameters
print("Optimized distances: ", d)
#verify the results
compute_phase_factors()
compute_input_field()
phase_mask[:] = phase_given_w(w_norm)
forward_propagate_1()
forward_propagate_2()
output_field1 = intermediate_fields_1[-1]
output_field2 = intermediate_fields_2[-1]
input_power = np.sum(np.abs(input_field)**2)
output_power = np.sum(np.abs(output_field1)**2)
print("Norm. input power: %.4e" % input_power)
print("Norm. output power: %.4e" % output_power)
#save the output field for later use
save_output_field = False
if save_output_field:
np.savetxt("results/optimized_output_field.txt", output_field1.reshape((Nx, Ny)))
# Plot results in individual PDF figures
# Reshape fields to 2D for visualization
output_field1_2d = output_field1.reshape((Nx, Ny))
output_field2_2d = output_field2.reshape((Nx, Ny))
phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(w_norm[S:2*S]).reshape((Nx, Ny))
phase_mask_3 = phase_given_w(w_norm[2*S:3*S]).reshape((Nx, Ny))
phase_mask_4 = phase_given_w(w_norm[3*S:4*S]).reshape((Nx, Ny))
phase_mask_zoom = 150
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_3, choose_quantity="phase_mask", save_name="phase_mask_3", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_4, choose_quantity="phase_mask", save_name="phase_mask_4", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
# Plot: Output field amplitude and phase
make_polarPlot_of(output_field1_2d, choose_quantity="amplitude", save_name="optimized_output_field_amplitude", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field1_2d, choose_quantity="phase", save_name="optimized_output_field_phase", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field2_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field2_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
slice_y_index = Ny//2
slice_x = xs * 1e6
slice_target = target1_Efield.reshape((Nx, Ny))[slice_y_index,:]
slice_output = output_field1.reshape((Nx, Ny))[slice_y_index,:]
#make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude")
#make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase")
#save the phase masks for later use
savePhase_masks = False
if savePhase_masks:
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2)
print("All plots saved to results/ folder.")
@@ -0,0 +1,954 @@
'''
This file implements a metasurface doublet for mode conversion. The optimization loop implements two beam paths that are
optimized independetly. This is useful for example when considering distinct polarizations (x and y). The optimization
is based on an epigraph formulation.
'''
import gc
import time
import multiprocessing
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
import os
if not os.path.exists("results"):
os.makedirs("results")
##############################
#physics and simulation domain
wavelength = 1.55e-6
n0 = 1.0
n1 = 1.0
n2 = 1.0
lda0 = wavelength / n0
lda1 = wavelength / n1
lda2 = wavelength / n2
k0 = 2 * np.pi / lda0
k1 = 2 * np.pi / lda1
k2 = 2 * np.pi / lda2
opt_max_eval = 270
unit_cell_pitch = 700e-9 #equivalent to spatial sampling
Nx = 1024 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d0_init = 300e-6 #propagation distance: source - MS1
d1_init = 500e-6 #propagation distance: MS1 - MS2
d2_init = 300e-6 #propagation distance: MS2 - target
distance_lower_bound = 100e-6
distance_upper_bound = 1000e-6
distance_num = 3
d = np.array([d0_init, d1_init, d2_init])
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#Source
beam_waist = 5.2e-6
#output fibre
output_fiber_core_diameter = 19e-6
##############################
##############################
#plan pyfftw objects for forward and inverse FFTs
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = np.zeros((Nx, Ny), dtype=complex)
P_0 = np.zeros((Nx, Ny), dtype=complex)
P_0_nat = np.zeros((Nx, Ny), dtype=complex)
phase_factor_1 = np.zeros((Nx, Ny), dtype=complex)
P_1 = np.zeros((Nx, Ny), dtype=complex)
P_1_nat = np.zeros((Nx, Ny), dtype=complex)
phase_factor_2 = np.zeros((Nx, Ny), dtype=complex)
P_2 = np.zeros((Nx, Ny), dtype=complex)
P_2_nat = np.zeros((Nx, Ny), dtype=complex)
P_1_dagger = np.zeros((Nx, Ny), dtype=complex)
P_2_dagger = np.zeros((Nx, Ny), dtype=complex)
P_1_dagger_nat = np.zeros((Nx, Ny), dtype=complex)
P_2_dagger_nat = np.zeros((Nx, Ny), dtype=complex)
##############################
def compute_phase_factors():
phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0[:] = np.exp(1j * phase_factor_0)
P_0_nat[:] = np.fft.ifftshift(P_0)
phase_factor_1[:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1[:] = np.exp(1j * phase_factor_1)
P_1_nat[:] = np.fft.ifftshift(P_1)
phase_factor_2[:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2[:] = np.exp(1j * phase_factor_2)
P_2_nat[:] = np.fft.ifftshift(P_2)
P_1_dagger[:] = P_1.T.conj()
P_2_dagger[:] = P_2.T.conj()
P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj()
##############################
#Plot zoom parameters [um]
#Plot zoom parameters [um]
zoom_x = 150
zoom_y = 150
zoom_output_fiber_radius = 25
full_view_x = size_x * 1e6 / 2
full_view_y = size_y * 1e6 / 2
##############################
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern(mode_list:list)->list:
'''Returns the fiber mode pattern for the selected n index.
Args:
mode_list: the list of the selected fiber mode indices
Returns:
The fiber mode patterns corresponding to the selected indices returned
as a list of 2D arrays with the same order as the input mode_list
'''
#check if the modes in mode_list have already been compute by checking
#the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files.
fiber_modes_input_folder = "fiber_modes"
output_patterns = []
try:
for enum_index, n in enumerate(mode_list):
#try to load mode index n
try:
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
pattern = np.load(pattern_file)
output_patterns.append(pattern)
print("Loaded fiber patterns for mode index: ", n)
#if mode index n was not found, compute it
except FileNotFoundError:
if not os.path.exists(fiber_modes_input_folder):
os.makedirs(fiber_modes_input_folder)
print("Computing fiber patterns for mode index: ", n)
lda = 1.55e-6
n_co = 1.4630 #core refractive index
n_cl = 1.4585 #cladding refractive index
fiber_core_diameter = output_fiber_core_diameter # [m]
a = fiber_core_diameter / 2 #fiber core radius
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
b_solutions_at_V_obs = []
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
b_mode_sel = b_solutions_at_V_obs[0]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi)
else:#outside core
E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi)
#save the computed pattern to a file for future use
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
np.save(pattern_file, E_field)
output_patterns.append(E_field)
return output_patterns
except:
raise(Exception("Error in computing fiber patterns"))
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the simulation spatial grid by a given physical (true space) amount.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X + shift_amount_x
Yp = Y + shift_amount_y
return Xp, Yp
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates.
Normalization consists in scaling the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The 2D normalized Gaussian source field.
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source(source_to_propagate):
'''Returns the propagated source field (flattened) just before metasurface 1 (MS1).
This involves propagating the source for a distance d0: from its plane of definition up to MS1.
Args:
source_to_propagate: the source field to be propagated (flattened)
Returns:
The propagated source field (flattened) just before MS1.
'''
fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny)))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
plt.plot(x_axis[i], y_axis[i])
plt.xlabel("x [um]")
plt.tight_layout()
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None:
'''Makes a polar plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_r: zoom level for the radial axis (in micrometers)
Returns:
None
'''
fig, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting
im = ax.imshow(given_field_2d,origin='lower')
elif choose_quantity == "phase":
given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting
im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("argument choose_quantity must be either 'amplitude' or 'phase'")
center_x, center_y = Nx//2, Ny//2
radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels
circle = patches.Circle((center_x, center_y), radius, transform=ax.transData)
im.set_clip_path(circle)
ax.set_xlim(center_x - radius, center_x + radius)
ax.set_ylim(center_y - radius, center_y + radius)
ax.axis('off')
cbar = plt.colorbar(im, ax=ax)
plt.savefig("results/" + save_name + ".pdf")
###########################
#initialize source field
source_field = get_source_field(beam_waist)
source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#source field integrated intensity
source_power = np.sum(np.abs(source_field)**2)
print("Source field integrated intensity: %.4e" % source_power)
#make a plot and save the source field
make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
###########################
###########################
#initialize input field (propagated source field)
input_field = np.zeros((S,), dtype=complex)
input_field_2d = np.zeros((Nx, Ny), dtype=complex)
#make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
def compute_input_field():
input_field[:] = propagate_source(source_field)
input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field integrated intensity
input_field_intensity = np.sum(np.abs(input_field)**2)
print("Input field integrated intensity: %.4e" % input_field_intensity)
###########################
###########################
#initialize target field
target_mode_indices = [1,2]
fiber_mode_pattern1, fiber_mode_pattern2 = get_fiber_mode_pattern(target_mode_indices)
target1_Efield_2d = fiber_mode_pattern1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern1)**2))
target2_Efield_2d = fiber_mode_pattern2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern2)**2))
target1_Efield = target1_Efield_2d.flatten() #normalized target field (intensity = 1)
target2_Efield = target2_Efield_2d.flatten() #normalized target field (intensity = 1)
# Plot: Target field
make_polarPlot_of(target1_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target1_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target2_Efield_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target2_Efield_2d, choose_quantity="phase", save_name="target_field_2_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
###########################
###########################
#initialize the normalized phase masks and global phase mask
w_norm = np.zeros((4*S)) #concatenated normalized parameters for both masks
X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny))
rho = np.sqrt(X**2 + Y**2)
circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0)
#w_norm[:S] += circular_mask_1
target1_pattern_phase = (np.angle(target1_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[S:2*S] += target1_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
target2_pattern_phase = (np.angle(target2_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[3*S:4*S] += target2_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
phase_mask = phase_given_w(w_norm)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
norm_phase_min = 0
norm_phase_max = 1
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
distance_upper_bounds = np.array([1, 1, 1])
distance_lower_bounds = np.array([0, 0, 0])
epigraph_upper_bounds = np.array([+np.inf])
epigraph_lower_bounds = np.array([-np.inf])
epigraph_distance_and_mask_upper_bounds = np.concatenate((epigraph_upper_bounds, distance_upper_bounds, mask_upper_bounds, mask_upper_bounds),axis=0)
epigraph_distance_and_mask_lower_bounds = np.concatenate((epigraph_lower_bounds, distance_lower_bounds, mask_lower_bounds, mask_lower_bounds),axis=0)
###########################
intermediate_fields_1 = [None, None]
intermediate_fields_2 = [None, None]
def forward_propagate_1() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields_1[0] = ifft_operator.output_array.copy()
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_1[0])**2))
fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:2*S].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields_1[1] = ifft_operator.output_array.copy()
end_time = time.time()
make_2Dplot_of(intermediate_fields_1[0], choose_quantity="amplitude", save_name="intermediate_field_1_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(intermediate_fields_1[1], choose_quantity="amplitude", save_name="intermediate_field_1_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def forward_propagate_2() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[2*S:3*S])).reshape((Nx, Ny))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields_2[0] = ifft_operator.output_array.copy()
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_2[0])**2))
fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[3*S:4*S].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields_2[1] = ifft_operator.output_array.copy()
end_time = time.time()
#make_2Dplot_of(intermediate_fields_2[0], choose_quantity="amplitude", save_name="intermediate_field_2_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(intermediate_fields_2[1], choose_quantity="amplitude", save_name="intermediate_field_2_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def adjoint_propagate_1():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:2*S]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d)) * target1_Efield_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_1[0].conj() * fft_input_array).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def adjoint_propagate_2():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[2*S:3*S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[3*S:4*S]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d)) * target2_Efield_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_2[0].conj() * fft_input_array).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def dist_grad_1():
P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j)
#print("shape of P_0_tilde: ", P_0_tilde.shape)
P_0_triangle = P_0 * P_0_tilde
P_0_triangle_nat = np.fft.ifftshift(P_0_triangle)
P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j)
P_1_triangle = P_1 * P_1_tilde
P_1_triangle_nat = np.fft.ifftshift(P_1_triangle)
P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j)
P_2_triangle = P_2 * P_2_tilde
P_2_triangle_nat = np.fft.ifftshift(P_2_triangle)
Phi_1 = np.exp(1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2 = np.exp(1j * phase_mask[S:2*S]).reshape((Nx, Ny))
Sf = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d))
S_dagger = np.conj(Sf)
E_t_dagger = np.conj(target1_Efield_2d)
################################
#dC_d0
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d1
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d2
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat
ifft_operator()
dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
return np.array([dC_d0, dC_d1, dC_d2])
def dist_grad_2():
P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j)
print("shape of P_0_tilde: ", P_0_tilde.shape)
P_0_triangle = P_0 * P_0_tilde
P_0_triangle_nat = np.fft.ifftshift(P_0_triangle)
P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j)
P_1_triangle = P_1 * P_1_tilde
P_1_triangle_nat = np.fft.ifftshift(P_1_triangle)
P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j)
P_2_triangle = P_2 * P_2_tilde
P_2_triangle_nat = np.fft.ifftshift(P_2_triangle)
Phi_1 = np.exp(1j * phase_mask[2*S:3*S]).reshape((Nx, Ny))
Phi_2 = np.exp(1j * phase_mask[3*S:4*S]).reshape((Nx, Ny))
Sf = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d))
S_dagger = np.conj(Sf)
E_t_dagger = np.conj(target2_Efield_2d)
################################
#dC_d0
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d1
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d2
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat
ifft_operator()
dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
return np.array([dC_d0, dC_d1, dC_d2])
def dist_norm_to_real(norm_dist):
'''Maps normalized distance parameters to real distance values.
Args:
norm_dist: normalized distance parameters (flattened)
Returns:
Real distance values corresponding to the normalized parameters (flattened)'''
return norm_dist * (distance_upper_bound - distance_lower_bound) + distance_lower_bound
def _compute_cost_1():
forward_propagate_1()
output_field_1 = intermediate_fields_1[-1].flatten()
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field_1 * np.conj(target1_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
def _compute_cost_2():
forward_propagate_2()
output_field_2 = intermediate_fields_2[-1].flatten()
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field_2 * np.conj(target2_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
opt_history_1 = []
opt_history_2 = []
opt_cycle = [0]
def cost_fun(x):
''' Cost function to be minimized.
Args:
x: input normalized parameter field (flattened)
grad: gradient of the cost function with respect to x, modified in place (flattened)
Returns:
The cost function value for the input parameter field x.
'''
d[:] = dist_norm_to_real(x[:distance_num]) #update distance variables
print("Current distances: ", d)
w_norm[:] = x[distance_num:] #update normalized phase mask parameters
phase_mask[:] = phase_given_w(w_norm)
compute_phase_factors() #update phase factor matrices
compute_input_field() #update input field based on the new distances
C1 = _compute_cost_1()
C2 = _compute_cost_2()
C = [-C1, -C2]
opt_history_1.append(C1)
opt_history_2.append(C2)
if opt_cycle[0] % 10 == 0:
plt.figure()
plt.plot(opt_history_1, 'r', label="Mode #1", markersize=2)
plt.plot(opt_history_2, 'b', label="Mode #2", markersize=2)
plt.xlabel('Iteration')
plt.ylabel('Cost Function Value')
plt.title('Optimization History')
plt.tight_layout()
plt.legend()
plt.savefig("results/modeconv1_optimization_history.pdf")
plt.close()
opt_cycle[0] = opt_cycle[0] + 1
grad = np.zeros((2, distance_num + 4*S)) #initialize gradient array
if opt_cycle[0] < opt_max_eval - 20:
grad[:,:distance_num] = np.zeros(distance_num) #set distance gradient to zero for the first 100 iterations
else:
grad[:,:distance_num] = (dist_grad_1() + dist_grad_2()) * (distance_upper_bound - distance_lower_bound)
#grad[:distance_num] = np.array([0,0,0]) #set distance gradient to zero for now
grad[0, distance_num:] = -np.concatenate((adjoint_propagate_1() * 2 * np.pi, np.zeros(2*S)))
grad[1, distance_num:] = -np.concatenate((np.zeros(2*S), adjoint_propagate_2() * 2 * np.pi))
gc.collect() #force garbage collection to free memory
plt.close('all')
return C, grad
def epigraph_constraint(result, epigraph_and_weights, gradient) -> None:
epigraph = epigraph_and_weights[0]
d = epigraph_and_weights[1:1+distance_num]
w_norm = epigraph_and_weights[1+distance_num:]
ob_vals, grad = cost_fun(np.concatenate((d, w_norm)))
if gradient.size > 0:
gradient[:,0] = -1
gradient[:,1:] = grad
result[:] = np.real(ob_vals) - epigraph
epi_opt_history = []
def epi_cost_fun(epigraph_and_weights, gradient):
epigraph = epigraph_and_weights[0]
if gradient.size > 0:
gradient[0] = 1
gradient[1:] = 0
epi_opt_history.append(epigraph)
return epigraph
if __name__ == "__main__":
#initialize nlopt solver
epigraph_tolerance = np.array([1e-4]*2)
solver = nlopt.opt(nlopt.LD_CCSAQ, 1 + distance_num + 4*S)
print(1 + distance_num + 4*S)
print(np.shape(epigraph_distance_and_mask_lower_bounds.flatten()))
solver.set_lower_bounds(epigraph_distance_and_mask_lower_bounds.flatten())
solver.set_upper_bounds(epigraph_distance_and_mask_upper_bounds.flatten())
solver.set_min_objective(epi_cost_fun)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-5)
solver.set_param("inner_maxeval", 5)
solver.set_param("verbosity",1)
solver.add_inequality_mconstraint(
lambda result_, epigraph_and_weights_, grad_: epigraph_constraint(
result_,
epigraph_and_weights_,
grad_,
),
epigraph_tolerance,
)
print("Starting optimization...")
start = True
initial_d_norm = np.array([0.4, 0.5, 0.3])
dist_and_w_norm = np.concatenate((initial_d_norm, w_norm), axis=0) #initialize optimization variables
epigraph_dist_and_w_norm = np.insert(dist_and_w_norm, 0, 0, axis=0) #initialize optimization variables with epigraph
if start:
epigraph_dist_and_w_norm[:] = solver.optimize(epigraph_dist_and_w_norm)
print("Optimization completed.")
final_epigraph = epigraph_dist_and_w_norm[0]
d[:] = dist_norm_to_real(epigraph_dist_and_w_norm[1:1+distance_num]) #update distance variables
w_norm[:] = epigraph_dist_and_w_norm[1+distance_num:] #update normalized phase mask parameters
print("Optimized distances: ", d)
#verify the results
compute_phase_factors()
compute_input_field()
phase_mask[:] = phase_given_w(w_norm)
forward_propagate_1()
forward_propagate_2()
output_field1 = intermediate_fields_1[-1]
output_field2 = intermediate_fields_2[-1]
input_power = np.sum(np.abs(input_field)**2)
output_power = np.sum(np.abs(output_field1)**2)
print("Norm. input power: %.4e" % input_power)
print("Norm. output power: %.4e" % output_power)
#save the output field for later use
save_output_field = False
if save_output_field:
np.savetxt("results/optimized_output_field.txt", output_field1.reshape((Nx, Ny)))
# Plot results in individual PDF figures
# Reshape fields to 2D for visualization
output_field1_2d = output_field1.reshape((Nx, Ny))
output_field2_2d = output_field2.reshape((Nx, Ny))
phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(w_norm[S:2*S]).reshape((Nx, Ny))
phase_mask_3 = phase_given_w(w_norm[2*S:3*S]).reshape((Nx, Ny))
phase_mask_4 = phase_given_w(w_norm[3*S:4*S]).reshape((Nx, Ny))
phase_mask_zoom = 150
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_3, choose_quantity="phase_mask", save_name="phase_mask_3", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_4, choose_quantity="phase_mask", save_name="phase_mask_4", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
# Plot: Output field amplitude and phase
make_polarPlot_of(output_field1_2d, choose_quantity="amplitude", save_name="optimized_output1_field_amplitude", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field1_2d, choose_quantity="phase", save_name="optimized_output1_field_phase", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field2_2d, choose_quantity="amplitude", save_name="optimized_output2_field_amplitude", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field2_2d, choose_quantity="phase", save_name="optimized_output2_field_phase", plot_zoom_r=zoom_output_fiber_radius)
#compute final overlap value
overlap1 = np.abs(np.sum(output_field1.flatten() * np.conj(target1_Efield))) ** 2
overlap2 = np.abs(np.sum(output_field2.flatten() * np.conj(target2_Efield))) ** 2
print("Final overlap with target mode 1: %.4e" % overlap1)
print("Final overlap with target mode 2: %.4e" % overlap2)
slice_y_index = Ny//2
slice_x = xs * 1e6
slice_target = target1_Efield.reshape((Nx, Ny))[slice_y_index,:]
slice_output = output_field1.reshape((Nx, Ny))[slice_y_index,:]
#make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude")
#make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase")
#save the phase masks for later use
savePhase_masks = False
if savePhase_masks:
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2)
print("All plots saved to results/ folder.")
+718
View File
@@ -0,0 +1,718 @@
import time
import multiprocessing
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
from smt.applications.ego import EGO, KRG
from smt.sampling_methods import LHS
from smt.design_space import (
DesignSpace,
)
import os
results_folder = "results_ego"
if not os.path.exists(results_folder):
os.makedirs(results_folder)
##############################
#physics and simulation domain
wavelength = 1.55e-6
n0 = 1.45
n1 = 1.0
n2 = 1.45
lda0 = wavelength / n0
lda1 = wavelength / n1
lda2 = wavelength / n2
k0 = 2 * np.pi / lda0
k1 = 2 * np.pi / lda1
k2 = 2 * np.pi / lda2
opt_max_eval = 150
unit_cell_pitch = 700e-9 #equivalent to spatial sampling
Nx = 1024 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d0 = 0 #propagation distance: source - MS1
d1 = 0 #propagation distance: MS1 - MS2
d2 = d0 #propagation distance: MS2 - target
d = np.zeros((3,))
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#Source
beam_waist = 5.2e-6
#output fibre
output_fiber_core_diameter = 19e-6
##############################
##############################
#plan pyfftw objects for forward and inverse FFTs
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = k0 * d[0] * np.sqrt(1 - (lda0 * nu_parallel) ** 2 + 0*1j)
P_0 = np.exp(1j * phase_factor_0)
P_0_nat = np.fft.ifftshift(P_0)
phase_factor_1 = k1 * d[1] * np.sqrt(1 - (lda1 * nu_parallel) ** 2 + 0*1j)
P_1 = np.exp(1j * phase_factor_1)
P_1_nat = np.fft.ifftshift(P_1)
phase_factor_2 = k2 * d[2] * np.sqrt(1 - (lda2 * nu_parallel) ** 2 + 0*1j)
P_2 = np.exp(1j * phase_factor_2)
P_2_nat = np.fft.ifftshift(P_2)
P_1_dagger = P_1.T.conj()
P_2_dagger = P_2.T.conj()
P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj()
##############################
##############################
#Plot zoom parameters [um]
zoom_x = 150
zoom_y = 150
zoom_output_fiber_radius = 63
full_view_x = size_x * 1e6 / 2
full_view_y = size_y * 1e6 / 2
##############################
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern(mode_list:list)->list:
'''Returns the fiber mode pattern for the selected n index.
Args:
mode_list: the list of the selected fiber mode indices
Returns:
The fiber mode patterns corresponding to the selected indices returned
as a list of 2D arrays with the same order as the input mode_list
'''
#check if the modes in mode_list have already been compute by checking
#the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files.
fiber_modes_input_folder = "fiber_modes"
output_patterns = []
try:
for enum_index, n in enumerate(mode_list):
#try to load mode index n
try:
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
pattern = np.load(pattern_file)
output_patterns.append(pattern)
print("Loaded fiber patterns for mode index: ", n)
#if mode index n was not found, compute it
except FileNotFoundError:
if not os.path.exists(fiber_modes_input_folder):
os.makedirs(fiber_modes_input_folder)
print("Computing fiber patterns for mode index: ", n)
lda = 1.55e-6
n_co = 1.4630 #core refractive index
n_cl = 1.4585 #cladding refractive index
fiber_core_diameter = output_fiber_core_diameter # [m]
a = fiber_core_diameter / 2 #fiber core radius
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
b_solutions_at_V_obs = []
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
b_mode_sel = b_solutions_at_V_obs[0]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi)
else:#outside core
E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi)
#save the computed pattern to a file for future use
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
np.save(pattern_file, E_field)
output_patterns.append(E_field)
return output_patterns
except:
raise(Exception("Error in computing fiber patterns"))
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the simulation spatial grid by a given physical (true space) amount.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X + shift_amount_x
Yp = Y + shift_amount_y
return Xp, Yp
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates.
Normalization consists in scaling the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The 2D normalized Gaussian source field.
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source(source_to_propagate):
'''Returns the propagated source field (flattened) just before metasurface 1 (MS1).
This involves propagating the source for a distance d0: from its plane of definition up to MS1.
Args:
source_to_propagate: the source field to be propagated (flattened)
Returns:
The propagated source field (flattened) just before MS1.
'''
fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny)))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
plt.plot(x_axis[i], y_axis[i])
plt.xlabel("x [um]")
plt.tight_layout()
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.savefig(results_folder + "/" + save_name + ".pdf")
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig(results_folder + "/" + save_name + ".pdf")
plt.close()
def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None:
'''Makes a polar plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_r: zoom level for the radial axis (in micrometers)
Returns:
None
'''
fig, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting
im = ax.imshow(given_field_2d,origin='lower')
elif choose_quantity == "phase":
given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting
im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
center_x, center_y = Nx//2, Ny//2
radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels
circle = patches.Circle((center_x, center_y), radius, transform=ax.transData)
im.set_clip_path(circle)
ax.set_xlim(center_x - radius, center_x + radius)
ax.set_ylim(center_y - radius, center_y + radius)
ax.axis('off')
cbar = plt.colorbar(im, ax=ax)
plt.savefig(results_folder + "/" + save_name + ".pdf")
###########################
#initialize source field
source_field = get_source_field(beam_waist)
source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#source field integrated intensity
source_power = np.sum(np.abs(source_field)**2)
print("Source field integrated intensity: %.4e" % source_power)
#make a plot and save the source field
make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
###########################
###########################
#initialize input field (propagated source field)
input_field = propagate_source(source_field)
input_field_2d = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#save the input field to numpy array
np.save(results_folder + "/input_field.npy", input_field_2d)
#input field integrated intensity
input_field_intensity = np.sum(np.abs(input_field)**2)
print("Input field integrated intensity: %.4e" % input_field_intensity)
#make a plot and save the input field
make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
###########################
###########################
#initialize target field
target_mode_indices = [2]
fiber_mode_pattern = get_fiber_mode_pattern(target_mode_indices)
target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2))
target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1)
# Plot: Target field
make_polarPlot_of(target_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
###########################
###########################
#initialize the normalized phase masks and global phase mask
w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks
X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny))
rho = np.sqrt(X**2 + Y**2)
circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0)
#w_norm[:S] += circular_mask_1
target_pattern_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[S:] += target_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
phase_mask = phase_given_w(w_norm)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
norm_phase_min = 0
norm_phase_max = 1
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
###########################
intermediate_fields = [None, None]
def forward_propagate() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy())
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2))
fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy())
end_time = time.time()
#make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def adjoint_propagate():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d)
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger)
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def _compute_cost():
forward_propagate()
output_field = intermediate_fields[-1].flatten()
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field * np.conj(target_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
iter_num = [0]
opt_history = []
def cost_fun(x, grad):
''' Cost function to be minimized.
Args:
x: input normalized parameter field (flattened)
grad: gradient of the cost function with respect to x, modified in place (flattened)
Returns:
The cost function value for the input parameter field x.
'''
w_norm[:] = x
phase_mask[:] = phase_given_w(w_norm)
C = _compute_cost()
opt_history.append(C)
iter_num[0] += 1
if iter_num[0] % 10 == 0:
plt.figure()
plt.plot(opt_history)
plt.xlabel('Iteration')
plt.ylabel('Cost Function Value')
plt.title('Optimization History')
plt.tight_layout()
plt.savefig(results_folder + "/modeconv1_optimization_history.pdf")
plt.close()
if grad.size > 0:
grad[:] = adjoint_propagate() * 2 * np.pi
return C
def metaprop():
#initialize nlopt solver
solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S)
solver.set_lower_bounds(mask_lower_bounds.flatten())
solver.set_upper_bounds(mask_upper_bounds.flatten())
solver.set_max_objective(cost_fun)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-7)
solver.set_param("verbosity",1)
print("Starting optimization...")
start = True
if start:
w_norm[:] = solver.optimize(w_norm)
print("Optimization completed.")
#verify the results
phase_mask = phase_given_w(w_norm)
forward_propagate()
field_input_ms2 = intermediate_fields[0]
output_field = intermediate_fields[-1]
input_power = np.sum(np.abs(input_field)**2)
output_power = np.sum(np.abs(output_field)**2)
print("Norm. input power: %.4e" % input_power)
print("Norm. output power: %.4e" % output_power)
#save the output field for later use
save_output_field = True
if save_output_field:
np.save(results_folder + "/intermediate_field_0.npy", intermediate_fields[0].reshape((Nx, Ny)))
np.save(results_folder + "/optimized_output_field.npy", output_field.reshape((Nx, Ny)))
# Plot results in individual PDF figures
# Reshape fields to 2D for visualization
output_field_2d = output_field.reshape((Nx, Ny))
phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(w_norm[S:]).reshape((Nx, Ny))
phase_mask_zoom = 150
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
# Plot: Output field amplitude and phase
make_polarPlot_of(output_field_2d, choose_quantity="amplitude", save_name="optimized_output_field_amplitude", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field_2d, choose_quantity="phase", save_name="optimized_output_field_phase", plot_zoom_r=zoom_output_fiber_radius)
slice_y_index = Ny//2
slice_x = xs * 1e6
slice_target = target_Efield.reshape((Nx, Ny))[slice_y_index,:]
slice_output = output_field.reshape((Nx, Ny))[slice_y_index,:]
make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude")
make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase")
#save the phase masks for later use
savePhase_masks = True
if savePhase_masks:
np.save(results_folder + "/optimized_phase_mask_1.npy", phase_mask_1)
np.save(results_folder + "/optimized_phase_mask_2.npy", phase_mask_2)
last_optimum_value = solver.last_optimum_value()
print("Last optimum value: %.4e" % last_optimum_value)
print(f"All plots saved to {results_folder}/ folder.")
return last_optimum_value
def update_phase_factors():
phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (lda0 * nu_parallel) ** 2 + 0*1j)
P_0[:] = np.exp(1j * phase_factor_0)
P_0_nat[:] = np.fft.ifftshift(P_0)
phase_factor_1[:] = k1 * d[1] * np.sqrt(1 - (lda1 * nu_parallel) ** 2 + 0*1j)
P_1[:] = np.exp(1j * phase_factor_1)
P_1_nat[:] = np.fft.ifftshift(P_1)
phase_factor_2[:] = k2 * d[2] * np.sqrt(1 - (lda2 * nu_parallel) ** 2 + 0*1j)
P_2[:] = np.exp(1j * phase_factor_2)
P_2_nat[:] = np.fft.ifftshift(P_2)
P_1_dagger[:] = P_1.T.conj()
P_2_dagger[:] = P_2.T.conj()
P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj()
def update_input_fields():
input_field[:] = propagate_source(source_field)
input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#save the input field to numpy array
#np.save("results/input_field.npy", input_field_2d)
#input field integrated intensity
input_field_intensity = np.sum(np.abs(input_field)**2)
print("Input field integrated intensity: %.4e" % input_field_intensity)
#make a plot and save the input field
make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
def initialize_phase_masks():
#initialize the normalized phase masks and global phase mask
w_norm[:] = np.zeros((2*S)) #concatenated normalized parameters for both masks
X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny))
rho = np.sqrt(X**2 + Y**2)
circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0)
#w_norm[:S] += circular_mask_1
target_pattern_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[S:] += target_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
phase_mask[:] = phase_given_w(w_norm)
def reset_globals():
opt_history[:] = []
w_norm[:] = np.zeros((2*S))
phase_mask[:] = phase_given_w(w_norm)
d[:] = np.zeros((3,))
def metaprop_update(x):
'''Objective function called from EGO loop. Given a set of input design parameters x,
it updates the system and calls the inner adjoint-based optimization.
Changing the system distance parameters requires updating the following terms:
1) propagation matrices
2) input field'''
if not os.path.exists(f'{results_folder}/metaprop_results.txt'):
with open(f'{results_folder}/metaprop_results.txt', 'w') as f:
f.write("d1 [m] d2 [m] d3 [m] ob1 ob2 t\n")
m, n = x.shape
C = np.zeros((m,1))
for i in range(m):
for j in range(n):
d[j] = 1e-6*np.round(x[i,j]) #update the global propagation distances with the new design parameters
d[-1] = d[0]
assert np.all(d), "Propagation distances must be real-positive."
print("Current propagation distances: ", d)
update_phase_factors() #update the phase factors for the new propagation distances
update_input_fields()
initialize_phase_masks()
print("USING D=",d)
last_optimum_value = metaprop() #solve the inner optimization problem for the given design parameters
#save all the partial results for a specific set of design parameters x to a text file
#in append mode, so that we can keep track of the optimization history
with open(f'{results_folder}/metaprop_results.txt', 'a') as f:
f.write(f"{d[0]:.6e} {d[1]:.6e} {d[2]:.6e} {last_optimum_value:.6e}\n")
print("Current cost function value: ", opt_history[-1])
C[i] = -last_optimum_value #remember ego does minimization
reset_globals()
return C
def run_ego():
##############################
#EGO setup
##############################
n_ego_iter = 30
xlimits = np.array([[100,700], [100,700]])
seed = 777
design_space = DesignSpace(xlimits, seed=seed)
criterion = 'EI'
ndoe = 20
#build DOE
sampling = LHS(xlimits=xlimits, seed=seed)
xdoe = sampling(ndoe)
sm = KRG(design_space=design_space, n_start=25, print_global=False)
ego = EGO(
n_iter=n_ego_iter,
criterion=criterion,
xdoe=xdoe,
surrogate=sm,
n_start=25,
)
x_opt, y_opt, ind_best, x_data, y_data = ego.optimize(fun=metaprop_update)
#save the optimization results to a text file
np.savetxt(f'{results_folder}/ego_opt_results.txt', np.hstack((x_opt,y_opt)))
if __name__ == "__main__":
run_ego()
+667
View File
@@ -0,0 +1,667 @@
import time
import multiprocessing
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
import os
save_folder = "results_sym"
if not os.path.exists(save_folder):
os.makedirs(save_folder)
##############################
#physics and simulation domain
wavelength = 1.55e-6
n0 = 1.45
n1 = 1.0
n2 = 1.45
lda0 = wavelength / n0
lda1 = wavelength / n1
lda2 = wavelength / n2
k0 = 2 * np.pi / lda0
k1 = 2 * np.pi / lda1
k2 = 2 * np.pi / lda2
opt_max_eval = 150
unit_cell_pitch = 700e-9 #equivalent to spatial sampling
Nx = 1024 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d0 = 403e-6 #propagation distance: source - MS1
d1 = 158e-6 #propagation distance: MS1 - MS2
d2 = d0 #propagation distance: MS2 - target
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#Source
beam_waist = 5.2e-6
#output fibre
output_fiber_core_diameter = 19e-6
##############################
##############################
#plan pyfftw objects for forward and inverse FFTs
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = k0 * d0 * np.sqrt(1 - (lda0 * nu_parallel) ** 2 + 0*1j)
P_0 = np.exp(1j * phase_factor_0)
P_0_nat = np.fft.ifftshift(P_0)
phase_factor_1 = k1 * d1 * np.sqrt(1 - (lda1 * nu_parallel) ** 2 + 0*1j)
P_1 = np.exp(1j * phase_factor_1)
P_1_nat = np.fft.ifftshift(P_1)
phase_factor_2 = k2 * d2 * np.sqrt(1 - (lda2 * nu_parallel) ** 2 + 0*1j)
P_2 = np.exp(1j * phase_factor_2)
P_2_nat = np.fft.ifftshift(P_2)
P_1_dagger = P_1.T.conj()
P_2_dagger = P_2.T.conj()
P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj()
##############################
##############################
#Plot zoom parameters [um]
zoom_x = 150
zoom_y = 150
zoom_output_fiber_radius = 63
full_view_x = size_x * 1e6 / 2
full_view_y = size_y * 1e6 / 2
##############################
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern(mode_list:list)->list:
'''Returns the fiber mode pattern for the selected n index.
Args:
mode_list: the list of the selected fiber mode indices
Returns:
The fiber mode patterns corresponding to the selected indices returned
as a list of 2D arrays with the same order as the input mode_list
'''
#check if the modes in mode_list have already been compute by checking
#the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files.
fiber_modes_input_folder = "fiber_modes"
output_patterns = []
try:
for enum_index, n in enumerate(mode_list):
#try to load mode index n
try:
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
pattern = np.load(pattern_file)
output_patterns.append(pattern)
print("Loaded fiber patterns for mode index: ", n)
#if mode index n was not found, compute it
except FileNotFoundError:
if not os.path.exists(fiber_modes_input_folder):
os.makedirs(fiber_modes_input_folder)
print("Computing fiber patterns for mode index: ", n)
lda = 1.55e-6
n_co = 1.4630 #core refractive index
n_cl = 1.4585 #cladding refractive index
fiber_core_diameter = output_fiber_core_diameter # [m]
a = fiber_core_diameter / 2 #fiber core radius
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
b_solutions_at_V_obs = []
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
b_mode_sel = b_solutions_at_V_obs[0]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi)
else:#outside core
E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi)
#save the computed pattern to a file for future use
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
np.save(pattern_file, E_field)
output_patterns.append(E_field)
return output_patterns
except:
raise(Exception("Error in computing fiber patterns"))
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the simulation spatial grid by a given physical (true space) amount.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X + shift_amount_x
Yp = Y + shift_amount_y
return Xp, Yp
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates.
Normalization consists in scaling the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The 2D normalized Gaussian source field.
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source(source_to_propagate):
'''Returns the propagated source field (flattened) just before metasurface 1 (MS1).
This involves propagating the source for a distance d0: from its plane of definition up to MS1.
Args:
source_to_propagate: the source field to be propagated (flattened)
Returns:
The propagated source field (flattened) just before MS1.
'''
fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny)))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
plt.plot(x_axis[i], y_axis[i])
plt.xlabel("x [um]")
plt.tight_layout()
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.savefig(f"{save_folder}/" + save_name + ".pdf")
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig(f"{save_folder}/" + save_name + ".pdf")
plt.close()
def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None:
'''Makes a polar plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_r: zoom level for the radial axis (in micrometers)
Returns:
None
'''
fig, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting
im = ax.imshow(given_field_2d,origin='lower')
elif choose_quantity == "phase":
given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting
im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
center_x, center_y = Nx//2, Ny//2
radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels
circle = patches.Circle((center_x, center_y), radius, transform=ax.transData)
im.set_clip_path(circle)
ax.set_xlim(center_x - radius, center_x + radius)
ax.set_ylim(center_y - radius, center_y + radius)
ax.axis('off')
cbar = plt.colorbar(im, ax=ax)
plt.savefig(f"{save_folder}/" + save_name + ".pdf")
def symmetrize(w, nx, ny):
'''
Takes an input array, turns it into a 2D array, and symmetrizes it along the x and y axes.
Returns the first quadrant of shape (nx,ny), flattened
'''
try:
w_2d = w.reshape((Nx, Ny))
except ValueError:
raise ValueError(f"Input array w cannot be reshaped to ({Nx}, {Ny}). Current shape: {w.shape}")
w_symm = np.zeros((nx, ny))
w_symm[:,:] = w_2d[:nx, :ny]
return w_symm.flatten()
def expand_symmetrize(w_symm, nx, ny):
'''
Takes a symmetrized array of shape (nx, ny) and expands it to a full 2D array of shape (Nx, Ny) by mirroring, flattened.
'''
w_full = np.zeros((Nx, Ny))
w_full[:nx, :ny] = w_symm.reshape((nx, ny))
w_full[:nx, ny:] = np.flip(w_symm.reshape((nx, ny)), axis=1)
w_full[nx:, :ny] = np.flip(w_symm.reshape((nx, ny)), axis=0)
w_full[nx:, ny:] = np.flip(np.flip(w_symm.reshape((nx, ny)), axis=0), axis=1)
return w_full.flatten()
###########################
#initialize source field
source_field = get_source_field(beam_waist)
source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#source field integrated intensity
source_power = np.sum(np.abs(source_field)**2)
print("Source field integrated intensity: %.4e" % source_power)
#make a plot and save the source field
make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
###########################
###########################
#initialize input field (propagated source field)
input_field = propagate_source(source_field)
input_field_2d = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#save the input field to numpy array
np.save(f"{save_folder}/input_field.npy", input_field_2d)
#input field integrated intensity
input_field_intensity = np.sum(np.abs(input_field)**2)
print("Input field integrated intensity: %.4e" % input_field_intensity)
#make a plot and save the input field
make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
###########################
###########################
#initialize target field
target_mode_indices = [2]
fiber_mode_pattern = get_fiber_mode_pattern(target_mode_indices)
target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2))
target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1)
# Plot: Target field
make_polarPlot_of(target_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
###########################
###########################
#initialize the normalized phase masks and global phase mask
w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks
X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny))
rho = np.sqrt(X**2 + Y**2)
circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0)
#w_norm[:S] += circular_mask_1
target_pattern_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[S:] += target_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
w_norm_symm_1 = symmetrize(w_norm[:S], Nx//2, Ny//2)
w_norm_symm_2 = symmetrize(w_norm[S:], Nx//2, Ny//2)
w_norm_symm = np.concatenate((w_norm_symm_1, w_norm_symm_2), axis=0)
phase_mask = phase_given_w(w_norm)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
norm_phase_min = 0
norm_phase_max = 1
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds_symm = mask_upper_bounds.reshape((Nx,Ny))[:Nx//2, :Ny//2].flatten()
mask_upper_bounds_symm = np.concatenate((mask_upper_bounds_symm, mask_upper_bounds_symm),axis=0)
mask_lower_bounds_symm = np.zeros_like(mask_upper_bounds_symm)
###########################
intermediate_fields = [None, None]
def forward_propagate() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy())
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2))
fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy())
end_time = time.time()
make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def reduce_symmetrized_gradient(g_full, nx, ny):
g_2d = g_full.reshape((Nx, Ny))
g_reduced = (
g_2d[:nx, :ny]
+ np.flip(g_2d[:nx, ny:], axis=1)
+ np.flip(g_2d[nx:, :ny], axis=0)
+ np.flip(np.flip(g_2d[nx:, ny:], axis=0), axis=1)
)
return g_reduced.flatten()
def adjoint_propagate():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d)
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger)
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return grad_C_phi_1, grad_C_phi_2
def _compute_cost():
forward_propagate()
output_field = intermediate_fields[-1].flatten()
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field * np.conj(target_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
opt_history = []
iter_num = [0]
def cost_fun(x, grad):
''' Cost function to be minimized.
Args:
x: input normalized parameter field (flattened)
grad: gradient of the cost function with respect to x, modified in place (flattened)
Returns:
The cost function value for the input parameter field x.
'''
#since the objective function is to maximize the overlap, and the target mode is C4 symmetric,
#we can let the algorithm optimize only a fourth of the phase mask and then synthetize automatically the remaining three quadrants.
#Since we have two metasurfaces, the total number of parameters the algorithm needs to optimize is S/2 (S/4 for each metasurface)
ms1_first_quadrant = x[:S//4].reshape(Nx//2, Ny//2)
ms2_first_quadrant = x[S//4:].reshape(Nx//2, Ny//2)
ms1_first_second = np.concatenate((ms1_first_quadrant, np.flip(ms1_first_quadrant, axis=1)), axis=1)
ms1 = np.concatenate((ms1_first_second, np.flip(ms1_first_second, axis=0)), axis=0)
ms2_first_second = np.concatenate((ms2_first_quadrant, np.flip(ms2_first_quadrant, axis=1)), axis=1)
ms2 = np.concatenate((ms2_first_second, np.flip(ms2_first_second, axis=0)), axis=0)
all_ms = np.concatenate((ms1.flatten(), ms2.flatten()))
w_norm[:] = all_ms
phase_mask[:] = phase_given_w(w_norm)
C = _compute_cost()
opt_history.append(C)
iter_num[0] += 1
if iter_num[0] % 10 == 0:
plt.figure()
plt.plot(opt_history)
plt.xlabel('Iteration')
plt.ylabel('Cost Function Value')
plt.title('Optimization History')
plt.tight_layout()
plt.savefig(f"{save_folder}/optimization_history.pdf")
plt.close()
if grad.size > 0:
full_grad1, full_grad2 = adjoint_propagate()
reduced_grad1 = reduce_symmetrized_gradient(full_grad1, Nx//2, Ny//2)
reduced_grad2 = reduce_symmetrized_gradient(full_grad2, Nx//2, Ny//2)
reduced_grads = np.concatenate((reduced_grad1, reduced_grad2), axis=0)
grad[:] = reduced_grads * 2 * np.pi
return C
if __name__ == "__main__":
#initialize nlopt solver
solver = nlopt.opt(nlopt.LD_CCSAQ, S//2)
solver.set_lower_bounds(mask_lower_bounds_symm.flatten())
solver.set_upper_bounds(mask_upper_bounds_symm.flatten())
solver.set_max_objective(cost_fun)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-7)
solver.set_param("verbosity",1)
print("Starting optimization...")
start = True
if start:
w_norm_symm[:] = solver.optimize(w_norm_symm)
w_norm[:S] = expand_symmetrize(w_norm_symm[:S//4], Nx//2, Ny//2)
w_norm[S:] = expand_symmetrize(w_norm_symm[S//4:], Nx//2, Ny//2)
print("Optimization completed.")
print("LAST OPTIMUM VALUE: ", solver.last_optimum_value())
#verify the results
phase_mask = phase_given_w(w_norm)
forward_propagate()
field_input_ms2 = intermediate_fields[0]
output_field = intermediate_fields[-1]
input_power = np.sum(np.abs(input_field)**2)
output_power = np.sum(np.abs(output_field)**2)
print("Norm. input power: %.4e" % input_power)
print("Norm. output power: %.4e" % output_power)
#save the output field for later use
save_output_field = True
if save_output_field:
np.save(f"{save_folder}/intermediate_field_0.npy", intermediate_fields[0].reshape((Nx, Ny)))
np.save(f"{save_folder}/optimized_output_field.npy", output_field.reshape((Nx, Ny)))
# Plot results in individual PDF figures
# Reshape fields to 2D for visualization
output_field_2d = output_field.reshape((Nx, Ny))
phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(w_norm[S:]).reshape((Nx, Ny))
phase_mask_zoom = 150
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
# Plot: Output field amplitude and phase
make_polarPlot_of(output_field_2d, choose_quantity="amplitude", save_name="optimized_output_field_amplitude", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field_2d, choose_quantity="phase", save_name="optimized_output_field_phase", plot_zoom_r=zoom_output_fiber_radius)
slice_y_index = Ny//2
slice_x = xs * 1e6
slice_target = target_Efield.reshape((Nx, Ny))[slice_y_index,:]
slice_output = output_field.reshape((Nx, Ny))[slice_y_index,:]
make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude")
make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase")
#save the phase masks for later use
savePhase_masks = True
if savePhase_masks:
np.save(f"{save_folder}/optimized_phase_mask_1.npy", phase_mask_1)
np.save(f"{save_folder}/optimized_phase_mask_2.npy", phase_mask_2)
print(f"All plots saved to {save_folder}/ folder.")
@@ -0,0 +1,717 @@
import time
import multiprocessing
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
import os
if not os.path.exists("results"):
os.makedirs("results")
##############################
#physics and simulation domain
wavelength = 1.55e-6
n0 = 1.0
n1 = 1.0
n2 = 1.0
lda0 = wavelength / n0
lda1 = wavelength / n1
lda2 = wavelength / n2
k0 = 2 * np.pi / lda0
k1 = 2 * np.pi / lda1
k2 = 2 * np.pi / lda2
opt_max_eval = 150
unit_cell_pitch = 700e-9 #equivalent to spatial sampling
Nx = 1024 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d0_init = 300e-6 #propagation distance: source - MS1
d1_init = 500e-6 #propagation distance: MS1 - MS2
d2_init = 400e-6 #propagation distance: MS2 - target
distance_lower_bound = 100e-6
distance_upper_bound = 1000e-6
distance_num = 3
d = np.array([d0_init, d1_init, d2_init])
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#Source
beam_waist = 5.2e-6
#output fibre
output_fiber_core_diameter = 19e-6
##############################
##############################
#plan pyfftw objects for forward and inverse FFTs
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = np.zeros((Nx, Ny), dtype=complex)
P_0 = np.zeros((Nx, Ny), dtype=complex)
P_0_nat = np.zeros((Nx, Ny), dtype=complex)
phase_factor_1 = np.zeros((Nx, Ny), dtype=complex)
P_1 = np.zeros((Nx, Ny), dtype=complex)
P_1_nat = np.zeros((Nx, Ny), dtype=complex)
phase_factor_2 = np.zeros((Nx, Ny), dtype=complex)
P_2 = np.zeros((Nx, Ny), dtype=complex)
P_2_nat = np.zeros((Nx, Ny), dtype=complex)
P_1_dagger = np.zeros((Nx, Ny), dtype=complex)
P_2_dagger = np.zeros((Nx, Ny), dtype=complex)
P_1_dagger_nat = np.zeros((Nx, Ny), dtype=complex)
P_2_dagger_nat = np.zeros((Nx, Ny), dtype=complex)
##############################
def compute_phase_factors():
phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0[:] = np.exp(1j * phase_factor_0)
P_0_nat[:] = np.fft.ifftshift(P_0)
phase_factor_1[:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1[:] = np.exp(1j * phase_factor_1)
P_1_nat[:] = np.fft.ifftshift(P_1)
phase_factor_2[:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2[:] = np.exp(1j * phase_factor_2)
P_2_nat[:] = np.fft.ifftshift(P_2)
P_1_dagger[:] = P_1.T.conj()
P_2_dagger[:] = P_2.T.conj()
P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj()
##############################
#Plot zoom parameters [um]
#Plot zoom parameters [um]
zoom_x = 150
zoom_y = 150
zoom_output_fiber_radius = 63
full_view_x = size_x * 1e6 / 2
full_view_y = size_y * 1e6 / 2
##############################
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern(mode_list:list)->list:
'''Returns the fiber mode pattern for the selected n index.
Args:
mode_list: the list of the selected fiber mode indices
Returns:
The fiber mode patterns corresponding to the selected indices returned
as a list of 2D arrays with the same order as the input mode_list
'''
#check if the modes in mode_list have already been compute by checking
#the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files.
fiber_modes_input_folder = "fiber_modes"
output_patterns = []
try:
for enum_index, n in enumerate(mode_list):
#try to load mode index n
try:
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
pattern = np.load(pattern_file)
output_patterns.append(pattern)
print("Loaded fiber patterns for mode index: ", n)
#if mode index n was not found, compute it
except FileNotFoundError:
if not os.path.exists(fiber_modes_input_folder):
os.makedirs(fiber_modes_input_folder)
print("Computing fiber patterns for mode index: ", n)
lda = 1.55e-6
n_co = 1.4630 #core refractive index
n_cl = 1.4585 #cladding refractive index
fiber_core_diameter = output_fiber_core_diameter # [m]
a = fiber_core_diameter / 2 #fiber core radius
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
b_solutions_at_V_obs = []
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
b_mode_sel = b_solutions_at_V_obs[0]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi)
else:#outside core
E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi)
#save the computed pattern to a file for future use
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
np.save(pattern_file, E_field)
output_patterns.append(E_field)
return output_patterns
except:
raise(Exception("Error in computing fiber patterns"))
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the simulation spatial grid by a given physical (true space) amount.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X + shift_amount_x
Yp = Y + shift_amount_y
return Xp, Yp
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates.
Normalization consists in scaling the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The 2D normalized Gaussian source field.
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source(source_to_propagate):
'''Returns the propagated source field (flattened) just before metasurface 1 (MS1).
This involves propagating the source for a distance d0: from its plane of definition up to MS1.
Args:
source_to_propagate: the source field to be propagated (flattened)
Returns:
The propagated source field (flattened) just before MS1.
'''
fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny)))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
plt.plot(x_axis[i], y_axis[i])
plt.xlabel("x [um]")
plt.tight_layout()
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None:
'''Makes a polar plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_r: zoom level for the radial axis (in micrometers)
Returns:
None
'''
fig, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting
im = ax.imshow(given_field_2d,origin='lower')
elif choose_quantity == "phase":
given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting
im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
center_x, center_y = Nx//2, Ny//2
radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels
circle = patches.Circle((center_x, center_y), radius, transform=ax.transData)
im.set_clip_path(circle)
ax.set_xlim(center_x - radius, center_x + radius)
ax.set_ylim(center_y - radius, center_y + radius)
ax.axis('off')
cbar = plt.colorbar(im, ax=ax)
plt.savefig("results/" + save_name + ".pdf")
###########################
#initialize source field
source_field = get_source_field(beam_waist)
source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#source field integrated intensity
source_power = np.sum(np.abs(source_field)**2)
print("Source field integrated intensity: %.4e" % source_power)
#make a plot and save the source field
make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
###########################
###########################
#initialize input field (propagated source field)
input_field = np.zeros((S,), dtype=complex)
input_field_2d = np.zeros((Nx, Ny), dtype=complex)
#make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
def compute_input_field():
input_field[:] = propagate_source(source_field)
input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field integrated intensity
input_field_intensity = np.sum(np.abs(input_field)**2)
print("Input field integrated intensity: %.4e" % input_field_intensity)
###########################
###########################
#initialize target field
target_mode_indices = [2]
fiber_mode_pattern = get_fiber_mode_pattern(target_mode_indices)
target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2))
target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1)
# Plot: Target field
make_polarPlot_of(target_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
###########################
###########################
#initialize the normalized phase masks and global phase mask
w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks
X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny))
rho = np.sqrt(X**2 + Y**2)
circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0)
#w_norm[:S] += circular_mask_1
target_pattern_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[S:] += target_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
phase_mask = phase_given_w(w_norm)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
norm_phase_min = 0
norm_phase_max = 1
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
distance_upper_bounds = np.array([1, 1, 1])
distance_lower_bounds = np.array([0, 0, 0])
distance_and_mask_upper_bounds = np.concatenate((distance_upper_bounds, mask_upper_bounds),axis=0)
distance_and_mask_lower_bounds = np.concatenate((distance_lower_bounds, mask_lower_bounds),axis=0)
###########################
intermediate_fields = [None, None]
def forward_propagate() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields[0] = ifft_operator.output_array.copy()
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2))
fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields[1] = ifft_operator.output_array.copy()
end_time = time.time()
make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def adjoint_propagate():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * fft_input_array).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def dist_grad():
P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j)
print("shape of P_0_tilde: ", P_0_tilde.shape)
P_0_triangle = P_0 * P_0_tilde
P_0_triangle_nat = np.fft.ifftshift(P_0_triangle)
P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j)
P_1_triangle = P_1 * P_1_tilde
P_1_triangle_nat = np.fft.ifftshift(P_1_triangle)
P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j)
P_2_triangle = P_2 * P_2_tilde
P_2_triangle_nat = np.fft.ifftshift(P_2_triangle)
Phi_1 = np.exp(1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2 = np.exp(1j * phase_mask[S:]).reshape((Nx, Ny))
Sf = np.sum(intermediate_fields[1] * np.conj(target_Efield_2d))
S_dagger = np.conj(Sf)
E_t_dagger = np.conj(target_Efield_2d)
################################
#dC_d0
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d1
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d2
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat
ifft_operator()
dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
return np.array([dC_d0, dC_d1, dC_d2])
def dist_norm_to_real(norm_dist):
'''Maps normalized distance parameters to real distance values.
Args:
norm_dist: normalized distance parameters (flattened)
Returns:
Real distance values corresponding to the normalized parameters (flattened)'''
return norm_dist * (distance_upper_bound - distance_lower_bound) + distance_lower_bound
def _compute_cost():
forward_propagate()
output_field = intermediate_fields[-1].flatten()
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field * np.conj(target_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
opt_history = []
opt_cycle = [0]
def cost_fun(x, grad):
''' Cost function to be minimized.
Args:
x: input normalized parameter field (flattened)
grad: gradient of the cost function with respect to x, modified in place (flattened)
Returns:
The cost function value for the input parameter field x.
'''
d[:] = dist_norm_to_real(x[:distance_num]) #update distance variables
print("Current distances: ", d)
w_norm[:] = x[distance_num:] #update normalized phase mask parameters
phase_mask[:] = phase_given_w(w_norm)
compute_phase_factors() #update phase factor matrices
compute_input_field() #update input field based on the new distances
C = _compute_cost()
opt_history.append(C)
plt.figure()
plt.plot(opt_history)
plt.xlabel('Iteration')
plt.ylabel('Cost Function Value')
plt.title('Optimization History')
plt.tight_layout()
plt.savefig("results/modeconv1_optimization_history.pdf")
plt.close()
opt_cycle[0] = opt_cycle[0] + 1
if grad.size > 0:
if opt_cycle[0] < 1000:
grad[:distance_num] = np.zeros(distance_num) #set distance gradient to zero for the first 100 iterations
else:
grad[:distance_num] = dist_grad() * (distance_upper_bound - distance_lower_bound)
#grad[:distance_num] = np.array([0,0,0]) #set distance gradient to zero for now
grad[distance_num:] = adjoint_propagate() * 2 * np.pi
return C
if __name__ == "__main__":
#initialize nlopt solver
solver = nlopt.opt(nlopt.LD_CCSAQ, distance_num + 2*S)
solver.set_lower_bounds(distance_and_mask_lower_bounds.flatten())
solver.set_upper_bounds(distance_and_mask_upper_bounds.flatten())
solver.set_max_objective(cost_fun)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-7)
solver.set_param("verbosity",1)
print("Starting optimization...")
start = True
initial_d = np.array([0.4, 0.5, 0.3])
dist_and_w_norm = np.concatenate((initial_d, w_norm), axis=0) #initialize optimization variables
if start:
dist_and_w_norm[:] = solver.optimize(dist_and_w_norm)
print("Optimization completed.")
d[:] = dist_norm_to_real(dist_and_w_norm[:distance_num]) #update distance variables
w_norm[:] = dist_and_w_norm[distance_num:] #update normalized phase mask parameters
print("Optimized distances: ", d)
#verify the results
compute_phase_factors()
compute_input_field()
phase_mask[:] = phase_given_w(w_norm)
forward_propagate()
output_field = intermediate_fields[-1]
input_power = np.sum(np.abs(input_field)**2)
output_power = np.sum(np.abs(output_field)**2)
print("Norm. input power: %.4e" % input_power)
print("Norm. output power: %.4e" % output_power)
#save the output field for later use
save_output_field = False
if save_output_field:
np.savetxt("results/optimized_output_field.txt", output_field.reshape((Nx, Ny)))
# Plot results in individual PDF figures
# Reshape fields to 2D for visualization
output_field_2d = output_field.reshape((Nx, Ny))
phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(w_norm[S:]).reshape((Nx, Ny))
phase_mask_zoom = 150
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
# Plot: Output field amplitude and phase
make_polarPlot_of(output_field_2d, choose_quantity="amplitude", save_name="optimized_output_field_amplitude", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field_2d, choose_quantity="phase", save_name="optimized_output_field_phase", plot_zoom_r=zoom_output_fiber_radius)
slice_y_index = Ny//2
slice_x = xs * 1e6
slice_target = target_Efield.reshape((Nx, Ny))[slice_y_index,:]
slice_output = output_field.reshape((Nx, Ny))[slice_y_index,:]
make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude")
make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase")
#save the phase masks for later use
savePhase_masks = True
if savePhase_masks:
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2)
print("All plots saved to results/ folder.")
@@ -0,0 +1,901 @@
import gc
import time
import multiprocessing
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
import os
if not os.path.exists("results"):
os.makedirs("results")
##############################
#physics and simulation domain
wavelength = 1.55e-6
n0 = 1.0
n1 = 1.0
n2 = 1.0
lda0 = wavelength / n0
lda1 = wavelength / n1
lda2 = wavelength / n2
k0 = 2 * np.pi / lda0
k1 = 2 * np.pi / lda1
k2 = 2 * np.pi / lda2
opt_max_eval = 250
unit_cell_pitch = 700e-9 #equivalent to spatial sampling
Nx = 1024 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d0_init = 300e-6 #propagation distance: source - MS1
d1_init = 500e-6 #propagation distance: MS1 - MS2
d2_init = 400e-6 #propagation distance: MS2 - target
distance_lower_bound = 100e-6
distance_upper_bound = 1000e-6
distance_num = 3
d = np.array([d0_init, d1_init, d2_init])
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#Source
beam_waist = 5.2e-6
#output fibre
output_fiber_core_diameter = 19e-6
##############################
##############################
#plan pyfftw objects for forward and inverse FFTs
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = np.zeros((Nx, Ny), dtype=complex)
P_0 = np.zeros((Nx, Ny), dtype=complex)
P_0_nat = np.zeros((Nx, Ny), dtype=complex)
phase_factor_1 = np.zeros((Nx, Ny), dtype=complex)
P_1 = np.zeros((Nx, Ny), dtype=complex)
P_1_nat = np.zeros((Nx, Ny), dtype=complex)
phase_factor_2 = np.zeros((Nx, Ny), dtype=complex)
P_2 = np.zeros((Nx, Ny), dtype=complex)
P_2_nat = np.zeros((Nx, Ny), dtype=complex)
P_1_dagger = np.zeros((Nx, Ny), dtype=complex)
P_2_dagger = np.zeros((Nx, Ny), dtype=complex)
P_1_dagger_nat = np.zeros((Nx, Ny), dtype=complex)
P_2_dagger_nat = np.zeros((Nx, Ny), dtype=complex)
##############################
def compute_phase_factors():
phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0[:] = np.exp(1j * phase_factor_0)
P_0_nat[:] = np.fft.ifftshift(P_0)
phase_factor_1[:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1[:] = np.exp(1j * phase_factor_1)
P_1_nat[:] = np.fft.ifftshift(P_1)
phase_factor_2[:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2[:] = np.exp(1j * phase_factor_2)
P_2_nat[:] = np.fft.ifftshift(P_2)
P_1_dagger[:] = P_1.T.conj()
P_2_dagger[:] = P_2.T.conj()
P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj()
##############################
#Plot zoom parameters [um]
#Plot zoom parameters [um]
zoom_x = 150
zoom_y = 150
zoom_output_fiber_radius = 25
full_view_x = size_x * 1e6 / 2
full_view_y = size_y * 1e6 / 2
##############################
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern(mode_list:list)->list:
'''Returns the fiber mode pattern for the selected n index.
Args:
mode_list: the list of the selected fiber mode indices
Returns:
The fiber mode patterns corresponding to the selected indices returned
as a list of 2D arrays with the same order as the input mode_list
'''
#check if the modes in mode_list have already been compute by checking
#the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files.
fiber_modes_input_folder = "fiber_modes"
output_patterns = []
try:
for enum_index, n in enumerate(mode_list):
#try to load mode index n
try:
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
pattern = np.load(pattern_file)
output_patterns.append(pattern)
print("Loaded fiber patterns for mode index: ", n)
#if mode index n was not found, compute it
except FileNotFoundError:
if not os.path.exists(fiber_modes_input_folder):
os.makedirs(fiber_modes_input_folder)
print("Computing fiber patterns for mode index: ", n)
lda = 1.55e-6
n_co = 1.4630 #core refractive index
n_cl = 1.4585 #cladding refractive index
fiber_core_diameter = output_fiber_core_diameter # [m]
a = fiber_core_diameter / 2 #fiber core radius
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
b_solutions_at_V_obs = []
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
b_mode_sel = b_solutions_at_V_obs[0]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi)
else:#outside core
E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi)
#save the computed pattern to a file for future use
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
np.save(pattern_file, E_field)
output_patterns.append(E_field)
return output_patterns
except:
raise(Exception("Error in computing fiber patterns"))
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the simulation spatial grid by a given physical (true space) amount.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X + shift_amount_x
Yp = Y + shift_amount_y
return Xp, Yp
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates.
Normalization consists in scaling the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The 2D normalized Gaussian source field.
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source(source_to_propagate):
'''Returns the propagated source field (flattened) just before metasurface 1 (MS1).
This involves propagating the source for a distance d0: from its plane of definition up to MS1.
Args:
source_to_propagate: the source field to be propagated (flattened)
Returns:
The propagated source field (flattened) just before MS1.
'''
fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny)))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
plt.plot(x_axis[i], y_axis[i])
plt.xlabel("x [um]")
plt.tight_layout()
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None:
'''Makes a polar plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_r: zoom level for the radial axis (in micrometers)
Returns:
None
'''
fig, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting
im = ax.imshow(given_field_2d,origin='lower')
elif choose_quantity == "phase":
given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting
im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
center_x, center_y = Nx//2, Ny//2
radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels
circle = patches.Circle((center_x, center_y), radius, transform=ax.transData)
im.set_clip_path(circle)
ax.set_xlim(center_x - radius, center_x + radius)
ax.set_ylim(center_y - radius, center_y + radius)
ax.axis('off')
cbar = plt.colorbar(im, ax=ax)
plt.savefig("results/" + save_name + ".pdf")
###########################
#initialize source field
source_field = get_source_field(beam_waist)
source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#source field integrated intensity
source_power = np.sum(np.abs(source_field)**2)
print("Source field integrated intensity: %.4e" % source_power)
#make a plot and save the source field
make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
###########################
###########################
#initialize input field (propagated source field)
input_field = np.zeros((S,), dtype=complex)
input_field_2d = np.zeros((Nx, Ny), dtype=complex)
#make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
def compute_input_field():
input_field[:] = propagate_source(source_field)
input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field integrated intensity
input_field_intensity = np.sum(np.abs(input_field)**2)
print("Input field integrated intensity: %.4e" % input_field_intensity)
###########################
###########################
#initialize target field
target_mode_indices = [1,2]
fiber_mode_pattern1, fiber_mode_pattern2 = get_fiber_mode_pattern(target_mode_indices)
target1_Efield_2d = fiber_mode_pattern1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern1)**2))
target2_Efield_2d = fiber_mode_pattern2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern2)**2))
target1_Efield = target1_Efield_2d.flatten() #normalized target field (intensity = 1)
target2_Efield = target2_Efield_2d.flatten() #normalized target field (intensity = 1)
# Plot: Target field
make_polarPlot_of(target1_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target1_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target2_Efield_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target2_Efield_2d, choose_quantity="phase", save_name="target_field_2_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
###########################
###########################
#initialize the normalized phase masks and global phase mask
w_norm = np.zeros((4*S)) #concatenated normalized parameters for both masks
X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny))
rho = np.sqrt(X**2 + Y**2)
circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0)
#w_norm[:S] += circular_mask_1
target1_pattern_phase = (np.angle(target1_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[S:2*S] += target1_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
target2_pattern_phase = (np.angle(target2_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[3*S:4*S] += target2_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
phase_mask = phase_given_w(w_norm)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
norm_phase_min = 0
norm_phase_max = 1
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
distance_upper_bounds = np.array([1, 1, 1])
distance_lower_bounds = np.array([0, 0, 0])
distance_and_mask_upper_bounds = np.concatenate((distance_upper_bounds, mask_upper_bounds, mask_upper_bounds),axis=0)
distance_and_mask_lower_bounds = np.concatenate((distance_lower_bounds, mask_lower_bounds, mask_lower_bounds),axis=0)
###########################
intermediate_fields_1 = [None, None]
intermediate_fields_2 = [None, None]
def forward_propagate_1() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields_1[0] = ifft_operator.output_array.copy()
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_1[0])**2))
fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:2*S].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields_1[1] = ifft_operator.output_array.copy()
end_time = time.time()
make_2Dplot_of(intermediate_fields_1[0], choose_quantity="amplitude", save_name="intermediate_field_1_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(intermediate_fields_1[1], choose_quantity="amplitude", save_name="intermediate_field_1_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def forward_propagate_2() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[2*S:3*S])).reshape((Nx, Ny))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields_2[0] = ifft_operator.output_array.copy()
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_2[0])**2))
fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[3*S:4*S].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields_2[1] = ifft_operator.output_array.copy()
end_time = time.time()
#make_2Dplot_of(intermediate_fields_2[0], choose_quantity="amplitude", save_name="intermediate_field_2_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(intermediate_fields_2[1], choose_quantity="amplitude", save_name="intermediate_field_2_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def adjoint_propagate_1():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:2*S]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d)) * target1_Efield_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_1[0].conj() * fft_input_array).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def adjoint_propagate_2():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[2*S:3*S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[3*S:4*S]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d)) * target2_Efield_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_2[0].conj() * fft_input_array).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def dist_grad_1():
P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j)
print("shape of P_0_tilde: ", P_0_tilde.shape)
P_0_triangle = P_0 * P_0_tilde
P_0_triangle_nat = np.fft.ifftshift(P_0_triangle)
P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j)
P_1_triangle = P_1 * P_1_tilde
P_1_triangle_nat = np.fft.ifftshift(P_1_triangle)
P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j)
P_2_triangle = P_2 * P_2_tilde
P_2_triangle_nat = np.fft.ifftshift(P_2_triangle)
Phi_1 = np.exp(1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2 = np.exp(1j * phase_mask[S:2*S]).reshape((Nx, Ny))
Sf = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d))
S_dagger = np.conj(Sf)
E_t_dagger = np.conj(target1_Efield_2d)
################################
#dC_d0
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d1
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d2
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat
ifft_operator()
dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
return np.array([dC_d0, dC_d1, dC_d2])
def dist_grad_2():
P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j)
print("shape of P_0_tilde: ", P_0_tilde.shape)
P_0_triangle = P_0 * P_0_tilde
P_0_triangle_nat = np.fft.ifftshift(P_0_triangle)
P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j)
P_1_triangle = P_1 * P_1_tilde
P_1_triangle_nat = np.fft.ifftshift(P_1_triangle)
P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j)
P_2_triangle = P_2 * P_2_tilde
P_2_triangle_nat = np.fft.ifftshift(P_2_triangle)
Phi_1 = np.exp(1j * phase_mask[2*S:3*S]).reshape((Nx, Ny))
Phi_2 = np.exp(1j * phase_mask[3*S:4*S]).reshape((Nx, Ny))
Sf = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d))
S_dagger = np.conj(Sf)
E_t_dagger = np.conj(target2_Efield_2d)
################################
#dC_d0
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d1
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d2
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat
ifft_operator()
dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
return np.array([dC_d0, dC_d1, dC_d2])
def dist_norm_to_real(norm_dist):
'''Maps normalized distance parameters to real distance values.
Args:
norm_dist: normalized distance parameters (flattened)
Returns:
Real distance values corresponding to the normalized parameters (flattened)'''
return norm_dist * (distance_upper_bound - distance_lower_bound) + distance_lower_bound
def _compute_cost_1():
forward_propagate_1()
output_field_1 = intermediate_fields_1[-1].flatten()
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field_1 * np.conj(target1_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
def _compute_cost_2():
forward_propagate_2()
output_field_2 = intermediate_fields_2[-1].flatten()
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field_2 * np.conj(target2_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
opt_history_1 = []
opt_history_2 = []
opt_cycle = [0]
def cost_fun(x, grad):
''' Cost function to be minimized.
Args:
x: input normalized parameter field (flattened)
grad: gradient of the cost function with respect to x, modified in place (flattened)
Returns:
The cost function value for the input parameter field x.
'''
d[:] = dist_norm_to_real(x[:distance_num]) #update distance variables
print("Current distances: ", d)
w_norm[:] = x[distance_num:] #update normalized phase mask parameters
phase_mask[:] = phase_given_w(w_norm)
compute_phase_factors() #update phase factor matrices
compute_input_field() #update input field based on the new distances
C1 = _compute_cost_1()
C2 = _compute_cost_2()
C = C1 + C2 #average cost function for both modes
opt_history_1.append(C1)
opt_history_2.append(C2)
if opt_cycle[0] % 10 == 0:
plt.figure()
plt.plot(opt_history_1, 'r', label="Mode #1", markersize=2)
plt.plot(opt_history_2, 'b', label="Mode #2", markersize=2)
plt.xlabel('Iteration')
plt.ylabel('Cost Function Value')
plt.title('Optimization History')
plt.tight_layout()
plt.legend()
plt.savefig("results/modeconv1_optimization_history.pdf")
plt.close()
opt_cycle[0] = opt_cycle[0] + 1
if grad.size > 0:
if opt_cycle[0] < 1000:
grad[:distance_num] = np.zeros(distance_num) #set distance gradient to zero for the first 100 iterations
else:
grad[:distance_num] = dist_grad_1() * (distance_upper_bound - distance_lower_bound) + dist_grad_2() * (distance_upper_bound - distance_lower_bound)
#grad[:distance_num] = np.array([0,0,0]) #set distance gradient to zero for now
grad[distance_num:2*S+distance_num] = adjoint_propagate_1() * 2 * np.pi
grad[2*S+distance_num:4*S+distance_num] = adjoint_propagate_2() * 2 * np.pi
gc.collect() #force garbage collection to free memory
plt.close('all')
return C
if __name__ == "__main__":
#initialize nlopt solver
solver = nlopt.opt(nlopt.LD_CCSAQ, distance_num + 4*S)
solver.set_lower_bounds(distance_and_mask_lower_bounds.flatten())
solver.set_upper_bounds(distance_and_mask_upper_bounds.flatten())
solver.set_max_objective(cost_fun)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-7)
solver.set_param("verbosity",1)
print("Starting optimization...")
start = True
initial_d = np.array([0.4, 0.5, 0.3])
dist_and_w_norm = np.concatenate((initial_d, w_norm), axis=0) #initialize optimization variables
if start:
dist_and_w_norm[:] = solver.optimize(dist_and_w_norm)
print("Optimization completed.")
d[:] = dist_norm_to_real(dist_and_w_norm[:distance_num]) #update distance variables
w_norm[:] = dist_and_w_norm[distance_num:] #update normalized phase mask parameters
print("Optimized distances: ", d)
#verify the results
compute_phase_factors()
compute_input_field()
phase_mask[:] = phase_given_w(w_norm)
forward_propagate_1()
forward_propagate_2()
output_field1 = intermediate_fields_1[-1]
output_field2 = intermediate_fields_2[-1]
input_power = np.sum(np.abs(input_field)**2)
output_power = np.sum(np.abs(output_field1)**2)
print("Norm. input power: %.4e" % input_power)
print("Norm. output power: %.4e" % output_power)
#save the output field for later use
save_output_field = False
if save_output_field:
np.savetxt("results/optimized_output_field.txt", output_field1.reshape((Nx, Ny)))
# Plot results in individual PDF figures
# Reshape fields to 2D for visualization
output_field1_2d = output_field1.reshape((Nx, Ny))
output_field2_2d = output_field2.reshape((Nx, Ny))
phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(w_norm[S:2*S]).reshape((Nx, Ny))
phase_mask_3 = phase_given_w(w_norm[2*S:3*S]).reshape((Nx, Ny))
phase_mask_4 = phase_given_w(w_norm[3*S:4*S]).reshape((Nx, Ny))
phase_mask_zoom = 150
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_3, choose_quantity="phase_mask", save_name="phase_mask_3", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_4, choose_quantity="phase_mask", save_name="phase_mask_4", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
# Plot: Output field amplitude and phase
make_polarPlot_of(output_field1_2d, choose_quantity="amplitude", save_name="optimized_output1_field_amplitude", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field1_2d, choose_quantity="phase", save_name="optimized_output1_field_phase", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field2_2d, choose_quantity="amplitude", save_name="optimized_output2_field_amplitude", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field2_2d, choose_quantity="phase", save_name="optimized_output2_field_phase", plot_zoom_r=zoom_output_fiber_radius)
slice_y_index = Ny//2
slice_x = xs * 1e6
slice_target = target1_Efield.reshape((Nx, Ny))[slice_y_index,:]
slice_output = output_field1.reshape((Nx, Ny))[slice_y_index,:]
#make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude")
#make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase")
#save the phase masks for later use
savePhase_masks = False
if savePhase_masks:
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2)
print("All plots saved to results/ folder.")
@@ -0,0 +1,954 @@
'''
This file implements a metasurface doublet for mode conversion. The optimization loop implements two beam paths that are
optimized independetly. This is useful for example when considering distinct polarizations (x and y). The optimization
is based on an epigraph formulation.
'''
import gc
import time
import multiprocessing
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
import os
if not os.path.exists("results"):
os.makedirs("results")
##############################
#physics and simulation domain
wavelength = 1.55e-6
n0 = 1.0
n1 = 1.0
n2 = 1.0
lda0 = wavelength / n0
lda1 = wavelength / n1
lda2 = wavelength / n2
k0 = 2 * np.pi / lda0
k1 = 2 * np.pi / lda1
k2 = 2 * np.pi / lda2
opt_max_eval = 270
unit_cell_pitch = 700e-9 #equivalent to spatial sampling
Nx = 1024 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d0_init = 300e-6 #propagation distance: source - MS1
d1_init = 500e-6 #propagation distance: MS1 - MS2
d2_init = 300e-6 #propagation distance: MS2 - target
distance_lower_bound = 100e-6
distance_upper_bound = 1000e-6
distance_num = 3
d = np.array([d0_init, d1_init, d2_init])
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#Source
beam_waist = 5.2e-6
#output fibre
output_fiber_core_diameter = 19e-6
##############################
##############################
#plan pyfftw objects for forward and inverse FFTs
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = np.zeros((Nx, Ny), dtype=complex)
P_0 = np.zeros((Nx, Ny), dtype=complex)
P_0_nat = np.zeros((Nx, Ny), dtype=complex)
phase_factor_1 = np.zeros((Nx, Ny), dtype=complex)
P_1 = np.zeros((Nx, Ny), dtype=complex)
P_1_nat = np.zeros((Nx, Ny), dtype=complex)
phase_factor_2 = np.zeros((Nx, Ny), dtype=complex)
P_2 = np.zeros((Nx, Ny), dtype=complex)
P_2_nat = np.zeros((Nx, Ny), dtype=complex)
P_1_dagger = np.zeros((Nx, Ny), dtype=complex)
P_2_dagger = np.zeros((Nx, Ny), dtype=complex)
P_1_dagger_nat = np.zeros((Nx, Ny), dtype=complex)
P_2_dagger_nat = np.zeros((Nx, Ny), dtype=complex)
##############################
def compute_phase_factors():
phase_factor_0[:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0[:] = np.exp(1j * phase_factor_0)
P_0_nat[:] = np.fft.ifftshift(P_0)
phase_factor_1[:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1[:] = np.exp(1j * phase_factor_1)
P_1_nat[:] = np.fft.ifftshift(P_1)
phase_factor_2[:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2[:] = np.exp(1j * phase_factor_2)
P_2_nat[:] = np.fft.ifftshift(P_2)
P_1_dagger[:] = P_1.T.conj()
P_2_dagger[:] = P_2.T.conj()
P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj()
##############################
#Plot zoom parameters [um]
#Plot zoom parameters [um]
zoom_x = 150
zoom_y = 150
zoom_output_fiber_radius = 25
full_view_x = size_x * 1e6 / 2
full_view_y = size_y * 1e6 / 2
##############################
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern(mode_list:list)->list:
'''Returns the fiber mode pattern for the selected n index.
Args:
mode_list: the list of the selected fiber mode indices
Returns:
The fiber mode patterns corresponding to the selected indices returned
as a list of 2D arrays with the same order as the input mode_list
'''
#check if the modes in mode_list have already been compute by checking
#the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files.
fiber_modes_input_folder = "fiber_modes"
output_patterns = []
try:
for enum_index, n in enumerate(mode_list):
#try to load mode index n
try:
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
pattern = np.load(pattern_file)
output_patterns.append(pattern)
print("Loaded fiber patterns for mode index: ", n)
#if mode index n was not found, compute it
except FileNotFoundError:
if not os.path.exists(fiber_modes_input_folder):
os.makedirs(fiber_modes_input_folder)
print("Computing fiber patterns for mode index: ", n)
lda = 1.55e-6
n_co = 1.4630 #core refractive index
n_cl = 1.4585 #cladding refractive index
fiber_core_diameter = output_fiber_core_diameter # [m]
a = fiber_core_diameter / 2 #fiber core radius
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
b_solutions_at_V_obs = []
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
b_mode_sel = b_solutions_at_V_obs[0]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi)
else:#outside core
E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi)
#save the computed pattern to a file for future use
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution_{Nx}.npy"
np.save(pattern_file, E_field)
output_patterns.append(E_field)
return output_patterns
except:
raise(Exception("Error in computing fiber patterns"))
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the simulation spatial grid by a given physical (true space) amount.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X + shift_amount_x
Yp = Y + shift_amount_y
return Xp, Yp
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates.
Normalization consists in scaling the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The 2D normalized Gaussian source field.
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source(source_to_propagate):
'''Returns the propagated source field (flattened) just before metasurface 1 (MS1).
This involves propagating the source for a distance d0: from its plane of definition up to MS1.
Args:
source_to_propagate: the source field to be propagated (flattened)
Returns:
The propagated source field (flattened) just before MS1.
'''
fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny)))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
plt.plot(x_axis[i], y_axis[i])
plt.xlabel("x [um]")
plt.tight_layout()
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None:
'''Makes a polar plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_r: zoom level for the radial axis (in micrometers)
Returns:
None
'''
fig, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting
im = ax.imshow(given_field_2d,origin='lower')
elif choose_quantity == "phase":
given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting
im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("argument choose_quantity must be either 'amplitude' or 'phase'")
center_x, center_y = Nx//2, Ny//2
radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels
circle = patches.Circle((center_x, center_y), radius, transform=ax.transData)
im.set_clip_path(circle)
ax.set_xlim(center_x - radius, center_x + radius)
ax.set_ylim(center_y - radius, center_y + radius)
ax.axis('off')
cbar = plt.colorbar(im, ax=ax)
plt.savefig("results/" + save_name + ".pdf")
###########################
#initialize source field
source_field = get_source_field(beam_waist)
source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#source field integrated intensity
source_power = np.sum(np.abs(source_field)**2)
print("Source field integrated intensity: %.4e" % source_power)
#make a plot and save the source field
make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
###########################
###########################
#initialize input field (propagated source field)
input_field = np.zeros((S,), dtype=complex)
input_field_2d = np.zeros((Nx, Ny), dtype=complex)
#make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
def compute_input_field():
input_field[:] = propagate_source(source_field)
input_field_2d[:] = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field integrated intensity
input_field_intensity = np.sum(np.abs(input_field)**2)
print("Input field integrated intensity: %.4e" % input_field_intensity)
###########################
###########################
#initialize target field
target_mode_indices = [1,2]
fiber_mode_pattern1, fiber_mode_pattern2 = get_fiber_mode_pattern(target_mode_indices)
target1_Efield_2d = fiber_mode_pattern1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern1)**2))
target2_Efield_2d = fiber_mode_pattern2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern2)**2))
target1_Efield = target1_Efield_2d.flatten() #normalized target field (intensity = 1)
target2_Efield = target2_Efield_2d.flatten() #normalized target field (intensity = 1)
# Plot: Target field
make_polarPlot_of(target1_Efield_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target1_Efield_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target2_Efield_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude_polar", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(target2_Efield_2d, choose_quantity="phase", save_name="target_field_2_phase_polar", plot_zoom_r=zoom_output_fiber_radius)
###########################
###########################
#initialize the normalized phase masks and global phase mask
w_norm = np.zeros((4*S)) #concatenated normalized parameters for both masks
X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny))
rho = np.sqrt(X**2 + Y**2)
circular_mask_1 = np.where(rho.flatten() < output_fiber_core_diameter/3, 1, 0)
#w_norm[:S] += circular_mask_1
target1_pattern_phase = (np.angle(target1_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[S:2*S] += target1_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
target2_pattern_phase = (np.angle(target2_Efield) + 2 * np.pi) % (2 * np.pi)*circular_mask_1 #make sure the phase is between 0 and 2pi
w_norm[3*S:4*S] += target2_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
phase_mask = phase_given_w(w_norm)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
norm_phase_min = 0
norm_phase_max = 1
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
distance_upper_bounds = np.array([1, 1, 1])
distance_lower_bounds = np.array([0, 0, 0])
epigraph_upper_bounds = np.array([+np.inf])
epigraph_lower_bounds = np.array([-np.inf])
epigraph_distance_and_mask_upper_bounds = np.concatenate((epigraph_upper_bounds, distance_upper_bounds, mask_upper_bounds, mask_upper_bounds),axis=0)
epigraph_distance_and_mask_lower_bounds = np.concatenate((epigraph_lower_bounds, distance_lower_bounds, mask_lower_bounds, mask_lower_bounds),axis=0)
###########################
intermediate_fields_1 = [None, None]
intermediate_fields_2 = [None, None]
def forward_propagate_1() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields_1[0] = ifft_operator.output_array.copy()
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_1[0])**2))
fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[S:2*S].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields_1[1] = ifft_operator.output_array.copy()
end_time = time.time()
make_2Dplot_of(intermediate_fields_1[0], choose_quantity="amplitude", save_name="intermediate_field_1_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(intermediate_fields_1[1], choose_quantity="amplitude", save_name="intermediate_field_1_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def forward_propagate_2() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = (input_field * np.exp(1j * phase_mask[2*S:3*S])).reshape((Nx, Ny))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields_2[0] = ifft_operator.output_array.copy()
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields_2[0])**2))
fft_input_array[:,:] = (ifft_operator.output_array * np.exp(1j * phase_mask[3*S:4*S].reshape((Nx, Ny))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields_2[1] = ifft_operator.output_array.copy()
end_time = time.time()
#make_2Dplot_of(intermediate_fields_2[0], choose_quantity="amplitude", save_name="intermediate_field_2_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(intermediate_fields_2[1], choose_quantity="amplitude", save_name="intermediate_field_2_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def adjoint_propagate_1():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:2*S]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d)) * target1_Efield_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_1[0].conj() * fft_input_array).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def adjoint_propagate_2():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[2*S:3*S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[3*S:4*S]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d)) * target2_Efield_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2_dagger
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields_2[0].conj() * fft_input_array).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * ifft_operator.output_array * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def dist_grad_1():
P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j)
#print("shape of P_0_tilde: ", P_0_tilde.shape)
P_0_triangle = P_0 * P_0_tilde
P_0_triangle_nat = np.fft.ifftshift(P_0_triangle)
P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j)
P_1_triangle = P_1 * P_1_tilde
P_1_triangle_nat = np.fft.ifftshift(P_1_triangle)
P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j)
P_2_triangle = P_2 * P_2_tilde
P_2_triangle_nat = np.fft.ifftshift(P_2_triangle)
Phi_1 = np.exp(1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2 = np.exp(1j * phase_mask[S:2*S]).reshape((Nx, Ny))
Sf = np.sum(intermediate_fields_1[1] * np.conj(target1_Efield_2d))
S_dagger = np.conj(Sf)
E_t_dagger = np.conj(target1_Efield_2d)
################################
#dC_d0
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d1
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d2
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat
ifft_operator()
dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
return np.array([dC_d0, dC_d1, dC_d2])
def dist_grad_2():
P_0_tilde = 1j * k0 * np.sqrt(1 - (lda0 * nu_parallel)**2 + 0*1j)
print("shape of P_0_tilde: ", P_0_tilde.shape)
P_0_triangle = P_0 * P_0_tilde
P_0_triangle_nat = np.fft.ifftshift(P_0_triangle)
P_1_tilde = 1j * k1 * np.sqrt(1 - (lda1 * nu_parallel)**2 + 0*1j)
P_1_triangle = P_1 * P_1_tilde
P_1_triangle_nat = np.fft.ifftshift(P_1_triangle)
P_2_tilde = 1j * k2 * np.sqrt(1 - (lda2 * nu_parallel)**2 + 0*1j)
P_2_triangle = P_2 * P_2_tilde
P_2_triangle_nat = np.fft.ifftshift(P_2_triangle)
Phi_1 = np.exp(1j * phase_mask[2*S:3*S]).reshape((Nx, Ny))
Phi_2 = np.exp(1j * phase_mask[3*S:4*S]).reshape((Nx, Ny))
Sf = np.sum(intermediate_fields_2[1] * np.conj(target2_Efield_2d))
S_dagger = np.conj(Sf)
E_t_dagger = np.conj(target2_Efield_2d)
################################
#dC_d0
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d0 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d1
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_triangle_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat
ifft_operator()
dC_d1 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
################################
#dC_d2
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_1
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat
ifft_operator()
fft_input_array[:,:] = ifft_operator.output_array * Phi_2
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_triangle_nat
ifft_operator()
dC_d2 = 2 * np.real(S_dagger * np.sum(E_t_dagger * ifft_operator.output_array))
return np.array([dC_d0, dC_d1, dC_d2])
def dist_norm_to_real(norm_dist):
'''Maps normalized distance parameters to real distance values.
Args:
norm_dist: normalized distance parameters (flattened)
Returns:
Real distance values corresponding to the normalized parameters (flattened)'''
return norm_dist * (distance_upper_bound - distance_lower_bound) + distance_lower_bound
def _compute_cost_1():
forward_propagate_1()
output_field_1 = intermediate_fields_1[-1].flatten()
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field_1 * np.conj(target1_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
def _compute_cost_2():
forward_propagate_2()
output_field_2 = intermediate_fields_2[-1].flatten()
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field_2 * np.conj(target2_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
opt_history_1 = []
opt_history_2 = []
opt_cycle = [0]
def cost_fun(x):
''' Cost function to be minimized.
Args:
x: input normalized parameter field (flattened)
grad: gradient of the cost function with respect to x, modified in place (flattened)
Returns:
The cost function value for the input parameter field x.
'''
d[:] = dist_norm_to_real(x[:distance_num]) #update distance variables
print("Current distances: ", d)
w_norm[:] = x[distance_num:] #update normalized phase mask parameters
phase_mask[:] = phase_given_w(w_norm)
compute_phase_factors() #update phase factor matrices
compute_input_field() #update input field based on the new distances
C1 = _compute_cost_1()
C2 = _compute_cost_2()
C = [-C1, -C2]
opt_history_1.append(C1)
opt_history_2.append(C2)
if opt_cycle[0] % 10 == 0:
plt.figure()
plt.plot(opt_history_1, 'r', label="Mode #1", markersize=2)
plt.plot(opt_history_2, 'b', label="Mode #2", markersize=2)
plt.xlabel('Iteration')
plt.ylabel('Cost Function Value')
plt.title('Optimization History')
plt.tight_layout()
plt.legend()
plt.savefig("results/modeconv1_optimization_history.pdf")
plt.close()
opt_cycle[0] = opt_cycle[0] + 1
grad = np.zeros((2, distance_num + 4*S)) #initialize gradient array
if opt_cycle[0] < opt_max_eval - 20:
grad[:,:distance_num] = np.zeros(distance_num) #set distance gradient to zero for the first 100 iterations
else:
grad[:,:distance_num] = (dist_grad_1() + dist_grad_2()) * (distance_upper_bound - distance_lower_bound)
#grad[:distance_num] = np.array([0,0,0]) #set distance gradient to zero for now
grad[0, distance_num:] = -np.concatenate((adjoint_propagate_1() * 2 * np.pi, np.zeros(2*S)))
grad[1, distance_num:] = -np.concatenate((np.zeros(2*S), adjoint_propagate_2() * 2 * np.pi))
gc.collect() #force garbage collection to free memory
plt.close('all')
return C, grad
def epigraph_constraint(result, epigraph_and_weights, gradient) -> None:
epigraph = epigraph_and_weights[0]
d = epigraph_and_weights[1:1+distance_num]
w_norm = epigraph_and_weights[1+distance_num:]
ob_vals, grad = cost_fun(np.concatenate((d, w_norm)))
if gradient.size > 0:
gradient[:,0] = -1
gradient[:,1:] = grad
result[:] = np.real(ob_vals) - epigraph
epi_opt_history = []
def epi_cost_fun(epigraph_and_weights, gradient):
epigraph = epigraph_and_weights[0]
if gradient.size > 0:
gradient[0] = 1
gradient[1:] = 0
epi_opt_history.append(epigraph)
return epigraph
if __name__ == "__main__":
#initialize nlopt solver
epigraph_tolerance = np.array([1e-4]*2)
solver = nlopt.opt(nlopt.LD_CCSAQ, 1 + distance_num + 4*S)
print(1 + distance_num + 4*S)
print(np.shape(epigraph_distance_and_mask_lower_bounds.flatten()))
solver.set_lower_bounds(epigraph_distance_and_mask_lower_bounds.flatten())
solver.set_upper_bounds(epigraph_distance_and_mask_upper_bounds.flatten())
solver.set_min_objective(epi_cost_fun)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-5)
solver.set_param("inner_maxeval", 5)
solver.set_param("verbosity",1)
solver.add_inequality_mconstraint(
lambda result_, epigraph_and_weights_, grad_: epigraph_constraint(
result_,
epigraph_and_weights_,
grad_,
),
epigraph_tolerance,
)
print("Starting optimization...")
start = True
initial_d_norm = np.array([0.4, 0.5, 0.3])
dist_and_w_norm = np.concatenate((initial_d_norm, w_norm), axis=0) #initialize optimization variables
epigraph_dist_and_w_norm = np.insert(dist_and_w_norm, 0, 0, axis=0) #initialize optimization variables with epigraph
if start:
epigraph_dist_and_w_norm[:] = solver.optimize(epigraph_dist_and_w_norm)
print("Optimization completed.")
final_epigraph = epigraph_dist_and_w_norm[0]
d[:] = dist_norm_to_real(epigraph_dist_and_w_norm[1:1+distance_num]) #update distance variables
w_norm[:] = epigraph_dist_and_w_norm[1+distance_num:] #update normalized phase mask parameters
print("Optimized distances: ", d)
#verify the results
compute_phase_factors()
compute_input_field()
phase_mask[:] = phase_given_w(w_norm)
forward_propagate_1()
forward_propagate_2()
output_field1 = intermediate_fields_1[-1]
output_field2 = intermediate_fields_2[-1]
input_power = np.sum(np.abs(input_field)**2)
output_power = np.sum(np.abs(output_field1)**2)
print("Norm. input power: %.4e" % input_power)
print("Norm. output power: %.4e" % output_power)
#save the output field for later use
save_output_field = False
if save_output_field:
np.savetxt("results/optimized_output_field.txt", output_field1.reshape((Nx, Ny)))
# Plot results in individual PDF figures
# Reshape fields to 2D for visualization
output_field1_2d = output_field1.reshape((Nx, Ny))
output_field2_2d = output_field2.reshape((Nx, Ny))
phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(w_norm[S:2*S]).reshape((Nx, Ny))
phase_mask_3 = phase_given_w(w_norm[2*S:3*S]).reshape((Nx, Ny))
phase_mask_4 = phase_given_w(w_norm[3*S:4*S]).reshape((Nx, Ny))
phase_mask_zoom = 150
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_3, choose_quantity="phase_mask", save_name="phase_mask_3", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
make_2Dplot_of(phase_mask_4, choose_quantity="phase_mask", save_name="phase_mask_4", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
# Plot: Output field amplitude and phase
make_polarPlot_of(output_field1_2d, choose_quantity="amplitude", save_name="optimized_output1_field_amplitude", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field1_2d, choose_quantity="phase", save_name="optimized_output1_field_phase", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field2_2d, choose_quantity="amplitude", save_name="optimized_output2_field_amplitude", plot_zoom_r=zoom_output_fiber_radius)
make_polarPlot_of(output_field2_2d, choose_quantity="phase", save_name="optimized_output2_field_phase", plot_zoom_r=zoom_output_fiber_radius)
#compute final overlap value
overlap1 = np.abs(np.sum(output_field1.flatten() * np.conj(target1_Efield))) ** 2
overlap2 = np.abs(np.sum(output_field2.flatten() * np.conj(target2_Efield))) ** 2
print("Final overlap with target mode 1: %.4e" % overlap1)
print("Final overlap with target mode 2: %.4e" % overlap2)
slice_y_index = Ny//2
slice_x = xs * 1e6
slice_target = target1_Efield.reshape((Nx, Ny))[slice_y_index,:]
slice_output = output_field1.reshape((Nx, Ny))[slice_y_index,:]
#make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target), np.abs(slice_output)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_amplitude")
#make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target)+2*np.pi)%(2*np.pi), (np.angle(slice_output)+2*np.pi)%(2*np.pi)], plot_zoom_x=1e6*output_fiber_core_diameter, save_name="modeconv_output_vs_target_phase")
#save the phase masks for later use
savePhase_masks = False
if savePhase_masks:
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2)
print("All plots saved to results/ folder.")
@@ -0,0 +1,659 @@
import time
import multiprocessing
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
##############################
#physics and simulation domain
wavelength = 1.55e-6
k0 = 2 * np.pi / wavelength
opt_max_eval = 250
unit_cell_pitch = 400e-9 #equivalent to spatial sampling
Nx = 2048 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d0 = 345e-6 #propagation distance: source - MS1
d1 = 500e-6 #propagation distance: MS1 - MS2
d2 = 300e-6 #propagation distance: MS2 - target
d = [d1, d2] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#optimization algorithm
epigraph_tolerance = np.array([1e-4]*2)
#epigraph_tolerance = np.array([1e-4])
##############################
##############################
#Source
beam_waist = 9e-6
source_spacing = 300e-6
##############################
##############################
#plan pyfftw objects for forward and inverse FFTs
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = k0 * d0 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0 = np.exp(1j * phase_factor_0)
P_0_nat = np.fft.ifftshift(P_0)
phase_factor_1 = k0 * d1 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1 = np.exp(1j * phase_factor_1)
P_1_nat = np.fft.ifftshift(P_1)
phase_factor_2 = k0 * d2 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2 = np.exp(1j * phase_factor_2)
P_2_nat = np.fft.ifftshift(P_2)
P_1_dagger = P_1.T.conj()
P_2_dagger = P_2.T.conj()
P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj()
##############################
##############################
#Plot zoom parameters [um]
zoom_x = 60
zoom_y = 60
zoom_core_diameter = 2 * 6
full_view_x = size_x * 1e6 / 2
full_view_y = size_y * 1e6 / 2
##############################
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern(mode_list:list)->list:
'''Returns the fiber mode pattern for the selected n index.
Args:
mode_list: the list of the selected fiber mode indices
Returns:
The fiber mode patterns corresponding to the selected indices returned
as a list of 2D arrays with the same order as the input mode_list
'''
#check if the modes in mode_list have already been compute by checking
#the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files.
print("Computing fiber patterns for mode indices: ", mode_list)
lda = 1.55e-6
n_co = 1.4630 #core refractive index
n_cl = 1.4585 #cladding refractive index
a = 6e-6 #fiber core radius
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
b_solutions_at_V_obs = []
for n in mode_list:
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
output_patterns = []
for enum_index,n_mode_sel in enumerate(mode_list):
b_mode_sel = b_solutions_at_V_obs[enum_index]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n_mode_sel,chi_co * r) / jv(n_mode_sel, chi_co * a) * np.cos(n_mode_sel * phi)
else:#outside core
E_field[i,j] = kv(n_mode_sel, chi_cl * r) / kv(n_mode_sel, chi_cl * a) * np.cos(n_mode_sel * phi)
output_patterns.append(E_field)
return output_patterns
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a normalized Gaussian source field for a given beam waist. Normalization consists in scaling
the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The normalized Gaussian source field
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source(source_to_propagate):
'''Returns the propagated source field (flattened) just before metasurface 1 (MS1). This involves propagating the
source for a distance d0: from its plane of definition up to MS1.'''
fft_input_array[:,:] = source_to_propagate.reshape((Nx, Ny))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = ifft_operator.output_array.flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,legend_labels,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
if len(legend_labels) != len(x_axis):
raise ValueError("Length of legend_labels must match the number of plots.")
else:
plt.plot(x_axis[i], y_axis[i], label=legend_labels[i])
plt.legend()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None:
'''Makes a polar plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_r: zoom level for the radial axis (in micrometers)
Returns:
None
'''
_, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting
im = ax.imshow(given_field_2d,origin='lower')
elif choose_quantity == "phase":
given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting
im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
center_x, center_y = Nx//2, Ny//2
radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels
circle = patches.Circle((center_x, center_y), radius, transform=ax.transData)
im.set_clip_path(circle)
ax.set_xlim(center_x - radius, center_x + radius)
ax.set_ylim(center_y - radius, center_y + radius)
ax.axis('off')
cbar = plt.colorbar(im, ax=ax)
plt.savefig("results/" + save_name + ".pdf")
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the spatial grid by a given amount. This is useful for centering the source field on the metasurface.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X + shift_amount_x
Yp = Y + shift_amount_y
return Xp, Yp
def shift_grid(array_to_shift,shift_amount_x,shift_amount_y):
'''Shifts a given 2D array by a given amount.
Args:
array_to_shift: the 2D array to be shifted
shift_amount_x: amount to shift the array in x direction (in meters)
shift_amount_y: amount to shift the array in y direction (in meters)
Returns:
the shifted 2D array'''
pass
###########################
#initialize source field
source_field_1 = get_source_field(beam_waist,center_x=-source_spacing/2,center_y=0)
source_field_1_2d = source_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting
source_field_2 = get_source_field(beam_waist,center_x=source_spacing/2,center_y=0)
source_field_2_2d = source_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting
#source field integrated intensity
source_1_power = np.sum(np.abs(source_field_1)**2)
source_2_power = np.sum(np.abs(source_field_2)**2)
print("Source 1 field integrated intensity: %.4e" % source_1_power)
print("Source 2 field integrated intensity: %.4e" % source_2_power)
#make a plot and save the source field
make_2Dplot_of(source_field_1_2d, choose_quantity="amplitude", save_name="source_1_field_amplitude", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y)
make_2Dplot_of(source_field_1_2d, choose_quantity="phase", save_name="source_1_field_phase", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y)
make_2Dplot_of(source_field_2_2d, choose_quantity="amplitude", save_name="source_2_field_amplitude", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y)
make_2Dplot_of(source_field_2_2d, choose_quantity="phase", save_name="source_2_field_phase", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y)
###########################
###########################
#initialize input field 1 (propagated source 1 field)
input_field_1 = propagate_source(source_field_1)
input_field_1_2d = input_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting
#initialize input field 2 (propagated source 2 field)
input_field_2 = propagate_source(source_field_2)
input_field_2_2d = input_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field 1 integrated intensity
input_field_1_intensity = np.sum(np.abs(input_field_1)**2)
print("Input field integrated intensity: %.4e" % input_field_1_intensity)
#input field 2 integrated intensity
input_field_2_intensity = np.sum(np.abs(input_field_2)**2)
print("Input field integrated intensity: %.4e" % input_field_2_intensity)
#make a plot and save the input field
make_2Dplot_of(input_field_1_2d, choose_quantity="amplitude", save_name="input_field_1_amplitude", plot_zoom_x=zoom_x*3, plot_zoom_y=zoom_y*3)
make_2Dplot_of(input_field_1_2d, choose_quantity="phase", save_name="input_field_1_phase", plot_zoom_x=zoom_x*3, plot_zoom_y=zoom_y*3)
make_2Dplot_of(input_field_2_2d, choose_quantity="amplitude", save_name="input_field_2_amplitude", plot_zoom_x=zoom_x*3, plot_zoom_y=zoom_y*3)
make_2Dplot_of(input_field_2_2d, choose_quantity="phase", save_name="input_field_2_phase", plot_zoom_x=zoom_x*3, plot_zoom_y=zoom_y*3)
#list of input fields to be propagated within the optimization loop
input_field_list = [input_field_1, input_field_2]
#input_field_list = [input_field_1]
###########################
###########################
#initialize target field
target_mode_indices = [1,2]
fiber_mode_pattern_1, fiber_mode_pattern_2 = get_fiber_mode_pattern(target_mode_indices)
target_Efield_1_2d = fiber_mode_pattern_1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_1)**2))
target_Efield_1 = target_Efield_1_2d.flatten() #normalized target field (intensity = 1)
target_Efield_2_2d = fiber_mode_pattern_2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_2)**2))
target_Efield_2 = target_Efield_2_2d.flatten() #normalized target field (intensity = 1)
# Plot: Target fields
make_2Dplot_of(target_Efield_1_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(target_Efield_1_2d, choose_quantity="phase", save_name="target_field_1_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(target_Efield_2_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(target_Efield_2_2d, choose_quantity="phase", save_name="target_field_2_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
target_field_list = [target_Efield_1, target_Efield_2]
#target_field_list = [target_Efield_1]
###########################
def forward_propagate(input_field:np.ndarray,weights:np.ndarray) -> np.ndarray:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
the output field computed at the observation plane
'''
print("Starting propagating...")
intermediate_fields = [None, None]
start_time = time.time()
phase_mask = phase_given_w(weights)
fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
#FIRST PROPAGATION
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy())
#print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2))
fft_input_array[:,:] = np.fft.ifftshift((np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny)))))
fft_operator()
#SECOND PROPAGATION
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy())
end_time = time.time()
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
return intermediate_fields
def adjoint_propagate(input_field_2d,target_Efield_2d,intermediate_fields):
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d)
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger)
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def value_and_grad(weights):
values = []
gradients = []
phase_mask[:] = phase_given_w(weights) #update phase mask in place
for i, input_field in enumerate(input_field_list):
intermediate_fields = forward_propagate(input_field, weights)
circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(intermediate_fields[-1].flatten() * circular_mask * np.conj(target_field_list[i]))) ** 2
C = np.real(np.sum(C_s))
values.append(-C)
gradients.append(
adjoint_propagate(
input_field_2d = input_field.reshape((Nx, Ny)),
target_Efield_2d = target_field_list[i].reshape((Nx, Ny)),
intermediate_fields = intermediate_fields)
)
return np.array(values), np.array(gradients)
obj_values1 = []
obj_values2 = []
def epigraph_constraint(result,epigraph_and_weights,gradient)->None:
'''Constraint function for the epigraph formulation
Args:
result: 1D array, modified in place, with the results of this constraint
evaluation
epigraph_and_weights: 1D array of epigraph (first element) and
design weights (remaining elements)
gradient: gradients, modified in place, of the constraint function with respect to the design weights
'''
epigraph = epigraph_and_weights[0]
weights = epigraph_and_weights[1:]
obj_val, grad = value_and_grad(weights)
obj_values1.append(obj_val[0])
obj_values2.append(obj_val[1])
if gradient.size > 0:
gradient[:,0] = -1
gradient[:,1:] = - grad * (2*np.pi)
#result of the evaluation of this constraint function
result[:] = np.real(obj_val) - epigraph
opt_history = []
def obj_fun(epigraph_and_weights, grad):
'''Objective function for the epigraph formulation
Args:
epigraph_and_weights: 1D array of epigraph (first element) and
design weights (remaining elements)
grad: gradient of the objective function with respect to the design weights, modified in place
Returns:
The scalar epigraph variable (the objective function)
'''
epigraph = epigraph_and_weights[0]
if grad.size > 0:
grad[0] = 1
grad[1:] = 0
opt_history.append(epigraph)
#make a 1d plot of the optimization history together with the objective values
make_1Dplot_of(x_axis=[np.arange(len(obj_values1)), np.arange(len(obj_values2)), np.arange(len(opt_history))], y_axis=[obj_values1, obj_values2, opt_history], legend_labels=["ob1","ob2","t"], save_name="optimization_history", plot_zoom_x=len(opt_history))
return epigraph
def initialize_masks():
X1,Y1 = shift_spatial_grid(-source_spacing/2,0)
rho1 = np.sqrt(X1**2 + Y1**2)
circular_mask_1 = np.where(rho1.flatten() < 10*6e-6, 1, 0)
X2,Y2 = shift_spatial_grid(source_spacing/2,0)
rho2 = np.sqrt(X2**2 + Y2**2)
circular_mask_2 = np.where(rho2.flatten() < 10*6e-6, 1, 0)
target_pattern_1_norm_phase = (np.angle(target_Efield_1) + 2 * np.pi) % (2 * np.pi) / (2*np.pi)
target_pattern_1_norm_phase = np.roll(target_pattern_1_norm_phase, shift=int(source_spacing/sampling_period/2), axis=0)
target_pattern_2_norm_phase = (np.angle(target_Efield_2) + 2 * np.pi) % (2 * np.pi) / (2*np.pi)
target_pattern_2_norm_phase = np.roll(target_pattern_2_norm_phase, shift=-int(source_spacing/sampling_period/2), axis=0)
#on metasurface 1, set initial guess on normalized phase elements
w_norm[:S] += circular_mask_1 * target_pattern_1_norm_phase + circular_mask_2 * target_pattern_2_norm_phase
#on metasurface 2, generate random initial guess for normalized phase elements
#w_norm[S:] += np.random.rand(S) * circular_mask_1 + np.random.rand(S) * circular_mask_2
if __name__ == "__main__":
###########################
#initialize the normalized phase masks and global phase mask
w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks
initialize_masks()
phase_mask = phase_given_w(w_norm)
#make an initial plot of the phase masks
make_2Dplot_of(phase_mask[:S].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(phase_mask[S:].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
norm_phase_min = 0.0
norm_phase_max = 1.0
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
epigraph_and_weights = np.insert(w_norm,0,0) #initialize epigraph variable to 0
print("#####Shape of epigraph_and_weights: ", epigraph_and_weights.shape)
###########################
#initialize nlopt solver
solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S + 1) #weights (2S) + epigraph (1)
epigraph_and_weights_lower_bounds = np.insert(mask_lower_bounds.flatten(),0,-np.inf)
epigraph_and_weights_upper_bounds = np.insert(mask_upper_bounds.flatten(),0,+np.inf)
solver.set_lower_bounds(epigraph_and_weights_lower_bounds)
solver.set_upper_bounds(epigraph_and_weights_upper_bounds)
solver.set_min_objective(obj_fun)
solver.add_inequality_mconstraint(
lambda result_, epigraph_and_weights_, grad_: epigraph_constraint(
result_,
epigraph_and_weights_,
grad_,
),
epigraph_tolerance,
)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-5)
solver.set_param("verbosity", 1)
solver.set_param("inner_maxeval", 10)
print("Starting optimization...")
start = True
if start:
epigraph_and_weights[:] = solver.optimize(epigraph_and_weights)
print("Optimization completed.")
#verify the results
opt_weights = epigraph_and_weights[1:]
verify_out_field_1 = forward_propagate(input_field_list[0], opt_weights)[-1]
verify_out_field_2 = forward_propagate(input_field_list[1], opt_weights)[-1]
all_source_on_field = input_field_list[0] + input_field_list[1]
verify_out_field_all = forward_propagate(all_source_on_field, opt_weights)[-1]
make_polarPlot_of(verify_out_field_1, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source1_amplitude", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_1, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source1_phase", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_2, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source2_amplitude", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_2, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source2_phase", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_all, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_allSource_amplitude", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_all, choose_quantity="phase", save_name="modeconv1_optimized_output_field_allSource_phase", plot_zoom_r=zoom_core_diameter)
#save the output fields for later use
saveFields = False
if saveFields:
np.savetxt("results/modeconv1_optimized_output_field_source1.txt", verify_out_field_1.reshape((Nx, Ny)))
np.savetxt("results/modeconv1_optimized_output_field_source2.txt", verify_out_field_2.reshape((Nx, Ny)))
np.savetxt("results/modeconv1_optimized_output_field_allSource.txt", verify_out_field_all.reshape((Nx, Ny)))
# Reshape fields to 2D for visualization
phase_mask_1 = phase_given_w(epigraph_and_weights[1:S+1]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(epigraph_and_weights[S+1:]).reshape((Nx, Ny))
# Plot: Phase masks
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#save the phase masks for later use
saveMasks = False
if saveMasks:
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2)
print("All plots saved to results/ folder.")
@@ -0,0 +1,780 @@
import time
import multiprocessing
import os
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
from smt.applications.ego import EGO, KRG
from smt.sampling_methods import LHS
from smt.design_space import (
DesignSpace,
)
##############################
#physics and simulation domain
wavelength = 1.55e-6
k0 = 2 * np.pi / wavelength
opt_max_eval = 150
unit_cell_pitch = 400e-9 #equivalent to spatial sampling
Nx = 2048 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d = [0, 0, 0] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target
d0 = d[0] #propagation distance: source - MS1
d1 = d[1] #propagation distance: MS1 - MS2
d2 = d[2] #propagation distance: MS2 - target
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#optimization algorithm
epigraph_tolerance = np.array([1e-4]*2)
#epigraph_tolerance = np.array([1e-4])
##############################
##############################
#Source
beam_waist = 9e-6
source_spacing = 300e-6
##############################
##############################
#plan pyfftw objects for forward and inverse FFTs
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#Plot zoom parameters [um]
zoom_x = 60
zoom_y = 60
zoom_core_diameter = 2 * 6
full_view_x = size_x * 1e6 / 2
full_view_y = size_y * 1e6 / 2
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0 = np.exp(1j * phase_factor_0)
P_0_nat = np.fft.ifftshift(P_0)
phase_factor_1 = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1 = np.exp(1j * phase_factor_1)
P_1_nat = np.fft.ifftshift(P_1)
phase_factor_2 = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2 = np.exp(1j * phase_factor_2)
P_2_nat = np.fft.ifftshift(P_2)
P_1_dagger = P_1.T.conj()
P_2_dagger = P_2.T.conj()
P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj()
##############################
def update_phase_factors():
print("Updating phase factors using d=",d)
phase_factor_0[:,:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0[:,:] = np.exp(1j * phase_factor_0)
P_0_nat[:,:] = np.fft.ifftshift(P_0)
phase_factor_1[:,:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1[:,:] = np.exp(1j * phase_factor_1)
P_1_nat[:,:] = np.fft.ifftshift(P_1)
phase_factor_2[:,:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2[:,:] = np.exp(1j * phase_factor_2)
P_2_nat[:,:] = np.fft.ifftshift(P_2)
P_1_dagger[:] = P_1.T.conj()
P_2_dagger[:] = P_2.T.conj()
P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj()
def update_input_fields():
input_field_1[:] = propagate_source(source_field_1)
input_field_1_2d[:] = input_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting
#initialize input field 2 (propagated source 2 field)
input_field_2[:] = propagate_source(source_field_2)
input_field_2_2d[:] = input_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field 1 integrated intensity
input_field_1_intensity = np.sum(np.abs(input_field_1)**2)
print("Input field integrated intensity: %.4e" % input_field_1_intensity)
#input field 2 integrated intensity
input_field_2_intensity = np.sum(np.abs(input_field_2)**2)
print("Input field integrated intensity: %.4e" % input_field_2_intensity)
#make a plot and save the input field
#make_2Dplot_of(input_field_1_2d, choose_quantity="amplitude", save_name="input_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(input_field_1_2d, choose_quantity="phase", save_name="input_field_1_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(input_field_2_2d, choose_quantity="amplitude", save_name="input_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(input_field_2_2d, choose_quantity="phase", save_name="input_field_2_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern(mode_list:list)->list:
'''Returns the fiber mode pattern for the selected n index.
Args:
mode_list: the list of the selected fiber mode indices
Returns:
The fiber mode patterns corresponding to the selected indices returned
as a list of 2D arrays with the same order as the input mode_list
'''
#check if the modes in mode_list have already been compute by checking
#the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files.
fiber_modes_input_folder = "fiber_modes"
output_patterns = []
try:
for enum_index, n in enumerate(mode_list):
#try to load mode index n
try:
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy"
pattern = np.load(pattern_file)
output_patterns.append(pattern)
print("Loaded fiber patterns for mode index: ", n)
#if mode index n was not found, compute it
except FileNotFoundError:
if not os.path.exists(fiber_modes_input_folder):
os.makedirs(fiber_modes_input_folder)
print("Computing fiber patterns for mode index: ", n)
lda = 1.55e-6
n_co = 1.4630 #core refractive index
n_cl = 1.4585 #cladding refractive index
fiber_core_diameter = 12e-6 # [m]
a = fiber_core_diameter / 2 #fiber core radius
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
b_solutions_at_V_obs = []
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
b_mode_sel = b_solutions_at_V_obs[0]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi)
else:#outside core
E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi)
#save the computed pattern to a file for future use
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy"
np.save(pattern_file, E_field)
output_patterns.append(E_field)
return output_patterns
except:
raise(Exception("Error in computing fiber patterns"))
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a normalized Gaussian source field for a given beam waist. Normalization consists in scaling
the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The normalized Gaussian source field
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source(source_to_propagate):
'''Returns the propagated source field (flattened) just before metasurface 1 (MS1). This involves propagating the
source for a distance d0: from its plane of definition up to MS1.'''
fft_input_array[:,:] = source_to_propagate.reshape((Nx, Ny))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = ifft_operator.output_array.flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,legend_labels,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
if len(legend_labels) != len(x_axis):
raise ValueError("Length of legend_labels must match the number of plots.")
else:
plt.plot(x_axis[i], y_axis[i], label=legend_labels[i])
plt.legend()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None:
'''Makes a polar plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_r: zoom level for the radial axis (in micrometers)
Returns:
None
'''
_, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting
im = ax.imshow(given_field_2d,origin='lower')
elif choose_quantity == "phase":
given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting
im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
center_x, center_y = Nx//2, Ny//2
radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels
circle = patches.Circle((center_x, center_y), radius, transform=ax.transData)
im.set_clip_path(circle)
ax.set_xlim(center_x - radius, center_x + radius)
ax.set_ylim(center_y - radius, center_y + radius)
ax.axis('off')
cbar = plt.colorbar(im, ax=ax)
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the spatial grid by a given amount. This is useful for centering the source field on the metasurface.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X + shift_amount_x
Yp = Y + shift_amount_y
return Xp, Yp
###########################
#initialize source field
source_field_1 = get_source_field(beam_waist,center_x=-source_spacing/2,center_y=0)
source_field_1_2d = source_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting
source_field_2 = get_source_field(beam_waist,center_x=source_spacing/2,center_y=0)
source_field_2_2d = source_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting
#source field integrated intensity
source_1_power = np.sum(np.abs(source_field_1)**2)
source_2_power = np.sum(np.abs(source_field_2)**2)
print("Source 1 field integrated intensity: %.4e" % source_1_power)
print("Source 2 field integrated intensity: %.4e" % source_2_power)
#make a plot and save the source field
#make_2Dplot_of(source_field_1_2d, choose_quantity="amplitude", save_name="source_1_field_amplitude", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y)
#make_2Dplot_of(source_field_1_2d, choose_quantity="phase", save_name="source_1_field_phase", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y)
#make_2Dplot_of(source_field_2_2d, choose_quantity="amplitude", save_name="source_2_field_amplitude", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y)
#make_2Dplot_of(source_field_2_2d, choose_quantity="phase", save_name="source_2_field_phase", plot_zoom_x=3*zoom_x, plot_zoom_y=3*zoom_y)
###########################
###########################
#initialize input field 1 (propagated source 1 field)
input_field_1 = propagate_source(source_field_1)
input_field_1_2d = input_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting
#initialize input field 2 (propagated source 2 field)
input_field_2 = propagate_source(source_field_2)
input_field_2_2d = input_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field 1 integrated intensity
input_field_1_intensity = np.sum(np.abs(input_field_1)**2)
print("Input field integrated intensity: %.4e" % input_field_1_intensity)
#input field 2 integrated intensity
input_field_2_intensity = np.sum(np.abs(input_field_2)**2)
print("Input field integrated intensity: %.4e" % input_field_2_intensity)
#list of input fields to be propagated within the optimization loop
input_field_list = [input_field_1, input_field_2]
#input_field_list = [input_field_1]
###########################
###########################
#initialize target field
target_mode_indices = [1,2]
fiber_mode_pattern_1, fiber_mode_pattern_2 = get_fiber_mode_pattern(target_mode_indices)
target_Efield_1_2d = fiber_mode_pattern_1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_1)**2))
target_Efield_1 = target_Efield_1_2d.flatten() #normalized target field (intensity = 1)
target_Efield_2_2d = fiber_mode_pattern_2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_2)**2))
target_Efield_2 = target_Efield_2_2d.flatten() #normalized target field (intensity = 1)
# Plot: Target fields
#make_2Dplot_of(target_Efield_1_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make_2Dplot_of(target_Efield_1_2d, choose_quantity="phase", save_name="target_field_1_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make_2Dplot_of(target_Efield_2_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make_2Dplot_of(target_Efield_2_2d, choose_quantity="phase", save_name="target_field_2_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
target_field_list = [target_Efield_1, target_Efield_2]
#target_field_list = [target_Efield_1]
###########################
def forward_propagate(input_field:np.ndarray,weights:np.ndarray) -> np.ndarray:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
the output field computed at the observation plane
'''
print("Starting propagating...")
intermediate_fields = [None, None]
start_time = time.time()
phase_mask = phase_given_w(weights)
fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
#FIRST PROPAGATION
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy())
#print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2))
fft_input_array[:,:] = np.fft.ifftshift((np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny)))))
fft_operator()
#SECOND PROPAGATION
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy())
end_time = time.time()
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
#make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
return intermediate_fields
def adjoint_propagate(input_field_2d,target_Efield_2d,intermediate_fields):
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d)
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger)
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def value_and_grad(weights):
values = []
gradients = []
phase_mask[:] = phase_given_w(weights) #update phase mask in place
for i, input_field in enumerate(input_field_list):
intermediate_fields = forward_propagate(input_field, weights)
circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(intermediate_fields[-1].flatten() * circular_mask * np.conj(target_field_list[i]))) ** 2
C = np.real(np.sum(C_s))
values.append(-C)
gradients.append(
adjoint_propagate(
input_field_2d = input_field.reshape((Nx, Ny)),
target_Efield_2d = target_field_list[i].reshape((Nx, Ny)),
intermediate_fields = intermediate_fields)
)
return np.array(values), np.array(gradients)
obj_values1 = []
obj_values2 = []
def epigraph_constraint(result,epigraph_and_weights,gradient)->None:
'''Constraint function for the epigraph formulation
Args:
result: 1D array, modified in place, with the results of this constraint
evaluation
epigraph_and_weights: 1D array of epigraph (first element) and
design weights (remaining elements)
gradient: gradients, modified in place, of the constraint function with respect to the design weights
'''
epigraph = epigraph_and_weights[0]
weights = epigraph_and_weights[1:]
obj_val, grad = value_and_grad(weights)
obj_values1.append(obj_val[0])
obj_values2.append(obj_val[1])
if gradient.size > 0:
gradient[:,0] = -1
gradient[:,1:] = - grad * (2*np.pi)
#result of the evaluation of this constraint function
result[:] = np.real(obj_val) - epigraph
opt_history = []
def obj_fun(epigraph_and_weights, grad):
'''Objective function for the epigraph formulation
Args:
epigraph_and_weights: 1D array of epigraph (first element) and
design weights (remaining elements)
grad: gradient of the objective function with respect to the design weights, modified in place
Returns:
The scalar epigraph variable (the objective function)
'''
epigraph = epigraph_and_weights[0]
if grad.size > 0:
grad[0] = 1
grad[1:] = 0
opt_history.append(epigraph)
#make a 1d plot of the optimization history together with the objective values
make_1Dplot_of(x_axis=[np.arange(len(obj_values1)), np.arange(len(obj_values2)), np.arange(len(opt_history))], y_axis=[obj_values1, obj_values2, opt_history], legend_labels=["ob1","ob2","t"], save_name="optimization_history", plot_zoom_x=len(opt_history))
return epigraph
def initialize_masks():
X1,Y1 = shift_spatial_grid(-source_spacing/2,0)
rho1 = np.sqrt(X1**2 + Y1**2)
circular_mask_1 = np.where(rho1.flatten() < 10*6e-6, 1, 0)
X2,Y2 = shift_spatial_grid(source_spacing/2,0)
rho2 = np.sqrt(X2**2 + Y2**2)
circular_mask_2 = np.where(rho2.flatten() < 10*6e-6, 1, 0)
target_pattern_1_norm_phase = (np.angle(target_Efield_1) + 2 * np.pi) % (2 * np.pi) / (2*np.pi)
target_pattern_1_norm_phase = np.roll(target_pattern_1_norm_phase, shift=int(source_spacing/sampling_period/2), axis=0)
target_pattern_2_norm_phase = (np.angle(target_Efield_2) + 2 * np.pi) % (2 * np.pi) / (2*np.pi)
target_pattern_2_norm_phase = np.roll(target_pattern_2_norm_phase, shift=-int(source_spacing/sampling_period/2), axis=0)
#on metasurface 1, set initial guess on normalized phase elements
w_norm[:S] += circular_mask_1 * target_pattern_1_norm_phase + circular_mask_2 * target_pattern_2_norm_phase
#on metasurface 2, generate random initial guess for normalized phase elements
#w_norm[S:] += np.random.rand(S) * circular_mask_1 + np.random.rand(S) * circular_mask_2
w_norm = np.zeros((2*S)) #global definition concatenated normalized parameters for both masks
phase_mask = phase_given_w(w_norm) #global definition
epigraph_and_weights = np.zeros((2*S + 1))
def metaprop_solve():
###########################
#initialize the normalized phase masks and global phase mask
initialize_masks()
phase_mask[:] = phase_given_w(w_norm)
#make an initial plot of the phase masks
#make_2Dplot_of(phase_mask[:S].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(phase_mask[S:].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
norm_phase_min = 0.0
norm_phase_max = 1.0
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
epigraph_and_weights[:] = np.insert(w_norm,0,0) #initialize epigraph variable to 0
print("#####Shape of epigraph_and_weights: ", epigraph_and_weights.shape)
###########################
#initialize nlopt solver
solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S + 1) #weights (2S) + epigraph (1)
epigraph_and_weights_lower_bounds = np.insert(mask_lower_bounds.flatten(),0,-np.inf)
epigraph_and_weights_upper_bounds = np.insert(mask_upper_bounds.flatten(),0,+np.inf)
solver.set_lower_bounds(epigraph_and_weights_lower_bounds)
solver.set_upper_bounds(epigraph_and_weights_upper_bounds)
solver.set_min_objective(obj_fun)
solver.add_inequality_mconstraint(
lambda result_, epigraph_and_weights_, grad_: epigraph_constraint(
result_,
epigraph_and_weights_,
grad_,
),
epigraph_tolerance,
)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-5)
solver.set_param("verbosity", 1)
solver.set_param("inner_maxeval", 5)
print("Starting optimization...")
start = True
if start:
epigraph_and_weights[:] = solver.optimize(epigraph_and_weights)
print("Optimization completed.")
#verify the results
opt_weights = epigraph_and_weights[1:]
verify_out_field_1 = forward_propagate(input_field_list[0], opt_weights)[-1]
verify_out_field_2 = forward_propagate(input_field_list[1], opt_weights)[-1]
all_source_on_field = input_field_list[0] + input_field_list[1]
verify_out_field_all = forward_propagate(all_source_on_field, opt_weights)[-1]
#make_polarPlot_of(verify_out_field_1, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source1_amplitude", plot_zoom_r=zoom_core_diameter)
#make_polarPlot_of(verify_out_field_1, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source1_phase", plot_zoom_r=zoom_core_diameter)
#make_polarPlot_of(verify_out_field_2, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source2_amplitude", plot_zoom_r=zoom_core_diameter)
#make_polarPlot_of(verify_out_field_2, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source2_phase", plot_zoom_r=zoom_core_diameter)
#make_polarPlot_of(verify_out_field_all, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_allSource_amplitude", plot_zoom_r=zoom_core_diameter)
#make_polarPlot_of(verify_out_field_all, choose_quantity="phase", save_name="modeconv1_optimized_output_field_allSource_phase", plot_zoom_r=zoom_core_diameter)
#save the output fields for later use
saveFields = False
if saveFields:
np.savetxt("results/modeconv1_optimized_output_field_source1.txt", verify_out_field_1.reshape((Nx, Ny)))
np.savetxt("results/modeconv1_optimized_output_field_source2.txt", verify_out_field_2.reshape((Nx, Ny)))
np.savetxt("results/modeconv1_optimized_output_field_allSource.txt", verify_out_field_all.reshape((Nx, Ny)))
# Reshape fields to 2D for visualization
phase_mask_1 = phase_given_w(epigraph_and_weights[1:S+1]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(epigraph_and_weights[S+1:]).reshape((Nx, Ny))
# Plot: Phase masks
#make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#save the phase masks for later use
saveMasks = False
if saveMasks:
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2)
print("All plots saved to results/ folder.")
def reset_globals():
obj_values1[:] = []
obj_values2[:] = []
opt_history[:] = []
w_norm[:] = np.zeros((2*S)) #reset the normalized phase masks for the next iteration
def metaprop(x):
#initialize results file if it does not exist
if not os.path.exists('results/metaprop_results.txt'):
with open('results/metaprop_results.txt', 'w') as f:
f.write("d1 [m] d2 [m] d3 [m] ob1 ob2 t\n")
m, n = x.shape
C = np.zeros((m,1))
for i in range(m):
for j in range(n):
d[j] = 1e-6*np.round(x[i,j]) #update the global propagation distances with the new design parameters
print("Current propagation distances: ", d)
update_phase_factors() #update the phase factors for the new propagation distances
update_input_fields()
metaprop_solve() #solve the inner optimization problem for the given design parameters
ob1 = obj_values1[-1]
ob2 = obj_values2[-1]
t = opt_history[-1]
#save all the partial results for a specific set of design parameters x to a text file
#in append mode, so that we can keep track of the optimization history
with open('results/metaprop_results.txt', 'a') as f:
f.write(f"{d[0]:.6e} {d[1]:.6e} {d[2]:.6e} {ob1:.6e} {ob2:.6e} {t:.6e}\n")
alpha = 0.5
C[i] = t + alpha * np.abs(ob1 - ob2) #cost function to be minimized by EGO)
reset_globals()
return C
def run_ego():
##############################
#EGO setup
##############################
n_ego_iter = 30
xlimits = np.array([[200,700], [200,700], [200,700]])
seed = 777
design_space = DesignSpace(xlimits, seed=seed)
criterion = 'EI'
ndoe = 20
#build DOE
sampling = LHS(xlimits=xlimits, seed=seed)
xdoe = sampling(ndoe)
sm = KRG(design_space=design_space, n_start=25, print_global=False)
ego = EGO(
n_iter=n_ego_iter,
criterion=criterion,
xdoe=xdoe,
surrogate=sm,
n_start=25,
)
x_opt, y_opt, ind_best, x_data, y_data = ego.optimize(fun=metaprop)
#save the optimization results to a text file
np.savetxt('results/ego_opt_results.txt', np.hstack((x_opt,y_opt)))
if __name__ == "__main__":
#if the results folder does not exist, create it
if not os.path.exists('results'):
os.makedirs('results')
run_ego()
@@ -0,0 +1,676 @@
import time
import multiprocessing
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
import os
##############################
#setup results folder
#if the results folder does not exist, create it
if not os.path.exists("results"):
os.makedirs("results")
##############################
#physics and simulation domain
wavelength = 1.55e-6
k0 = 2 * np.pi / wavelength
opt_max_eval = 300
unit_cell_pitch = 400e-9 #equivalent to spatial sampling
Nx = 2048 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d0 = 345e-6 #propagation distance: source - MS1
d1 = 500e-6 #propagation distance: MS1 - MS2
d2 = 300e-6 #propagation distance: MS2 - target
d = [d1, d2] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#Source
beam_waist = 4e-6
source_spacing = 300e-6
##############################
##############################
#PYFFTW PLANNING SETUP
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = k0 * d0 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0 = np.exp(1j * phase_factor_0)
P_0_nat = np.fft.ifftshift(P_0)
phase_factor_1 = k0 * d1 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1 = np.exp(1j * phase_factor_1)
P_1_nat = np.fft.ifftshift(P_1)
phase_factor_2 = k0 * d2 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2 = np.exp(1j * phase_factor_2)
P_2_nat = np.fft.ifftshift(P_2)
P_1_dagger = P_1.T.conj()
P_2_dagger = P_2.T.conj()
P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj()
##############################
##############################
#Plot zoom parameters [um]
zoom_x = 350
zoom_core_diameter = 2 * 6
zoom_x_outmode = 12
zoom_y = 350
zoom_y_outmode = 50
full_view_x = size_x * 1e6 / 2
full_view_y = size_y * 1e6 / 2
##############################
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern(mode_list:list)->list:
'''Returns the fiber mode pattern for the selected n index.
Args:
mode_list: the list of the selected fiber mode indices
Returns:
The fiber mode patterns corresponding to the selected indices returned
as a list of 2D arrays with the same order as the input mode_list
'''
print("Computing fiber patterns for mode indices: ", mode_list)
lda = 1.55e-6
n_co = 1.4630 #core refractive index
n_cl = 1.4585 #cladding refractive index
a = 6e-6 #fiber core radius
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
b_solutions_at_V_obs = []
for n in mode_list:
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
output_patterns = []
for enum_index,n_mode_sel in enumerate(mode_list):
b_mode_sel = b_solutions_at_V_obs[enum_index]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n_mode_sel,chi_co * r) / jv(n_mode_sel, chi_co * a) * np.cos(n_mode_sel * phi)
else:#outside core
E_field[i,j] = kv(n_mode_sel, chi_cl * r) / kv(n_mode_sel, chi_cl * a) * np.cos(n_mode_sel * phi)
output_patterns.append(E_field)
return output_patterns
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates.
Normalization consists in scaling the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The 2D normalized Gaussian source field.
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source(source_to_propagate):
'''Returns the propagated source field (flattened) just before metasurface 1 (MS1).
This involves propagating the source for a distance d0: from its plane of definition up to MS1.
Args:
source_to_propagate: the source field to be propagated (flattened)
Returns:
The propagated source field (flattened) just before MS1.
'''
fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny)))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
plt.plot(x_axis[i], y_axis[i])
plt.xlabel("x [um]")
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None:
'''Makes a polar plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_r: zoom level for the radial axis (in micrometers)
Returns:
None
'''
fig, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting
im = ax.imshow(given_field_2d,origin='lower')
elif choose_quantity == "phase":
given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting
im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
center_x, center_y = Nx//2, Ny//2
radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels
circle = patches.Circle((center_x, center_y), radius, transform=ax.transData)
im.set_clip_path(circle)
ax.set_xlim(center_x - radius, center_x + radius)
ax.set_ylim(center_y - radius, center_y + radius)
ax.axis('off')
cbar = plt.colorbar(im, ax=ax)
plt.savefig("results/" + save_name + ".pdf")
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the simulation spatial grid by a given physical (true space) amount.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X + shift_amount_x
Yp = Y + shift_amount_y
return Xp, Yp
###########################
#initialize source field
source_field_1 = get_source_field(beam_waist, center_x=-source_spacing/2, center_y=0)
source_field_2 = get_source_field(beam_waist, center_x=source_spacing/2, center_y=0)
#source field integrated intensity
source_1_power = np.sum(np.abs(source_field_1)**2)
source_2_power = np.sum(np.abs(source_field_2)**2)
print("Source 1 field integrated intensity: %.4e" % source_1_power)
print("Source 2 field integrated intensity: %.4e" % source_2_power)
#make a plot and save the source field
make_2Dplot_of(source_field_1, choose_quantity="amplitude", save_name="source_1_field_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(source_field_1, choose_quantity="phase", save_name="source_1_field_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(source_field_2, choose_quantity="amplitude", save_name="source_2_field_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(source_field_2, choose_quantity="phase", save_name="source_2_field_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
###########################
###########################
#initialize input field 1 (propagated source 1 field)
input_field_1 = propagate_source(source_field_1)
input_field_1_2d = input_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting
#initialize input field 2 (propagated source 2 field)
input_field_2 = propagate_source(source_field_2)
input_field_2_2d = input_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field 1 integrated intensity
input_field_1_intensity = np.sum(np.abs(input_field_1)**2)
print("Input field integrated intensity: %.4e" % input_field_1_intensity)
#input field 2 integrated intensity
input_field_2_intensity = np.sum(np.abs(input_field_2)**2)
print("Input field integrated intensity: %.4e" % input_field_2_intensity)
#make a plot and save the input field
make_2Dplot_of(input_field_1_2d, choose_quantity="amplitude", save_name="input_field_1_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(input_field_1_2d, choose_quantity="phase", save_name="input_field_1_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(input_field_2_2d, choose_quantity="amplitude", save_name="input_field_2_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(input_field_2_2d, choose_quantity="phase", save_name="input_field_2_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#list of input fields to be propagated within the optimization loop
input_field_list = [input_field_1, input_field_2]
###########################
###########################
#initialize target field
target_mode_indices = [1,2]
fiber_mode_pattern_1, fiber_mode_pattern_2 = get_fiber_mode_pattern(target_mode_indices)
target_Efield_1_2d = fiber_mode_pattern_1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_1)**2))
target_Efield_1 = target_Efield_1_2d.flatten() #normalized target field (intensity = 1)
target_Efield_2_2d = fiber_mode_pattern_2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_2)**2))
target_Efield_2 = target_Efield_2_2d.flatten() #normalized target field (intensity = 1)
# Plot target fields using polar coordinates
make_polarPlot_of(target_Efield_1_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(target_Efield_1_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(target_Efield_2_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude_polar", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(target_Efield_2_2d, choose_quantity="phase", save_name="target_field_2_phase_polar", plot_zoom_r=zoom_core_diameter)
target_field_list = [target_Efield_1, target_Efield_2]
###########################
def forward_propagate(input_field:np.ndarray,weights:np.ndarray) -> np.ndarray:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
the output field computed at the observation plane
'''
print("Starting propagating...")
intermediate_fields = [None, None]
start_time = time.time()
phase_mask = phase_given_w(weights)
fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
#FIRST PROPAGATION
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy())
#print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2))
fft_input_array[:,:] = np.fft.ifftshift((np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny)))))
fft_operator()
#SECOND PROPAGATION
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy())
end_time = time.time()
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
return intermediate_fields
def adjoint_propagate(input_field_2d,target_Efield_2d,intermediate_fields,input_weights):
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradients of the cost function with respect to the phase mask parameters (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
phase_mask = phase_given_w(input_weights)
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d)
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger)
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def value_and_grad(weights):
'''
Returns the value of the cost function and its gradient with respect to the phase mask parameters for
every available set of input/target fields.
Args:
weights: the current phase mask parameters (flattened)
Returns:
values: the value of the cost function for each input/target field pair
gradients: the gradient of the cost function with respect to the phase mask parameters for each input/target field pair
'''
values = []
gradients = []
phase_mask[:] = phase_given_w(weights) #update phase mask in place
for i, input_field in enumerate(input_field_list):
intermediate_fields = forward_propagate(input_field, weights)
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(intermediate_fields[-1].flatten() * np.conj(target_field_list[i]))) ** 2
C = np.real(np.sum(C_s))
values.append(C)
gradients.append(
adjoint_propagate(
input_field_2d = input_field.reshape((Nx, Ny)),
target_Efield_2d = target_field_list[i].reshape((Nx, Ny)),
intermediate_fields = intermediate_fields,
input_weights = weights
)
)
return np.array(values), np.array(gradients)
opt_history = []
opt_history1 = []
opt_history2 = []
def obj_fun(weights, grad):
'''
Objective function for the optimization. Computes the overall cost function by taking the mean value
of the individual cost functions for each input/target field pair. Also computes the gradient of the overall cost function
with respect to the weights parameters.
Args:
weights: the current normalized phase mask parameters (flattened)
grad: the gradient of the overall cost function with respect to the weights parameters (flattened)
Returns:
average_obj_val: the average value of the cost function across all input/target field pairs
'''
w_norm[:] = weights
obj_val, grads = value_and_grad(weights)
average_obj_val = np.mean(obj_val)
if grads.size > 0:
target_num = len(target_field_list)
if target_num > 1:
grads_sum = grads[0] + grads[1]
else:
grads_sum = grads[0]
grad[:] = 1 / target_num * (grads_sum) * 2 * np.pi
#make a plot of the magnitude of the gradient
make_2Dplot_of(np.abs(grad[S:].reshape((Nx, Ny))), choose_quantity="amplitude", save_name="gradient_magnitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
opt_history.append(average_obj_val)
opt_history1.append(obj_val[0])
opt_history2.append(obj_val[1])
make_1Dplot_of([np.arange(len(opt_history))]*3, [opt_history,opt_history1,opt_history2], plot_zoom_x=len(opt_history), save_name="optimization_history")
return average_obj_val
def initialize_masks():
X1,Y1 = shift_spatial_grid(-source_spacing/2,0)
rho1 = np.sqrt(X1**2 + Y1**2)
circular_mask_1 = np.where(rho1.flatten() < 3*beam_waist, 1, 0)
X2,Y2 = shift_spatial_grid(source_spacing/2,0)
rho2 = np.sqrt(X2**2 + Y2**2)
circular_mask_2 = np.where(rho2.flatten() < 3*beam_waist, 1, 0)
target_pattern_1_norm_phase = (np.angle(target_Efield_1) + 2 * np.pi) % (2 * np.pi) / (2*np.pi)
target_pattern_1_norm_phase = np.roll(target_pattern_1_norm_phase, shift=int(source_spacing/sampling_period/2), axis=0)
target_pattern_2_norm_phase = (np.angle(target_Efield_2) + 2 * np.pi) % (2 * np.pi) / (2*np.pi)
target_pattern_2_norm_phase = np.roll(target_pattern_2_norm_phase, shift=-int(source_spacing/sampling_period/2), axis=0)
#on metasurface 1, set initial guess on normalized phase elements
w_norm[:S] += circular_mask_1 * target_pattern_1_norm_phase + circular_mask_2 * target_pattern_2_norm_phase
if __name__ == "__main__":
###########################
#initialize the normalized phase masks and global phase mask
w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks
initialize_masks()
phase_mask = phase_given_w(w_norm)
#make an initial plot of the phase masks
make_2Dplot_of(phase_mask[:S].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(phase_mask[S:].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
norm_phase_min = 0.0
norm_phase_max = 1.0
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
###########################
#initialize nlopt solver
solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S) #weights (2S) + epigraph (1)
weights_lower_bounds = mask_lower_bounds.flatten()
weights_upper_bounds = mask_upper_bounds.flatten()
solver.set_lower_bounds(weights_lower_bounds)
solver.set_upper_bounds(weights_upper_bounds)
solver.set_max_objective(obj_fun)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-7)
solver.set_param("verbosity",1)
print("Starting optimization...")
start = True
if start:
w_norm[:] = solver.optimize(w_norm)
print("Optimization completed.")
#verify the results
opt_weights = w_norm
if len(input_field_list) > 1:
verify_out_field_1 = forward_propagate(input_field_list[0], opt_weights)[-1]
verify_out_field_2 = forward_propagate(input_field_list[1], opt_weights)[-1]
all_source_on_field = input_field_list[0] + input_field_list[1]
verify_out_field_all = forward_propagate(all_source_on_field, opt_weights)[-1]
make_polarPlot_of(verify_out_field_1, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source1_amplitude", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_1, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source1_phase", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_2, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source2_amplitude", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_2, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source2_phase", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_all, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_allSource_amplitude", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_all, choose_quantity="phase", save_name="modeconv1_optimized_output_field_allSource_phase", plot_zoom_r=zoom_core_diameter)
#make 1D slices of target/output fields to compare
slice_y_index = Ny//2
slice_x = xs * 1e6
slice_target_1 = target_Efield_1.reshape((Nx, Ny))[slice_y_index,:]
slice_target_2 = target_Efield_2.reshape((Nx, Ny))[slice_y_index,:]
slice_output_1 = verify_out_field_1.reshape((Nx, Ny))[slice_y_index,:]
slice_output_2 = verify_out_field_2.reshape((Nx, Ny))[slice_y_index,:]
slice_output_all = verify_out_field_all.reshape((Nx, Ny))[slice_y_index,:]
#amplitude and phase target 1
make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target_1), np.abs(slice_output_1)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_amplitude")
make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target_1)+2*np.pi)%(2*np.pi), (np.angle(slice_output_1)+2*np.pi)%(2*np.pi)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_phase")
#amplitude and phase target 2
make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target_2), np.abs(slice_output_2)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source2_amplitude")
make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target_2)+2*np.pi)%(2*np.pi), (np.angle(slice_output_2)+2*np.pi)%(2*np.pi)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source2_phase")
else:
verify_out_field_1 = forward_propagate(input_field_list[0], opt_weights)[-1]
make_2Dplot_of(verify_out_field_1, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source1_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(verify_out_field_1, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source1_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make a slice of the target/output fields to compare
slice_y_index = Ny//2
slice_x = xs * 1e6
slice_target_1 = target_Efield_1.reshape((Nx, Ny))[slice_y_index,:]
slice_output_1 = verify_out_field_1.reshape((Nx, Ny))[slice_y_index,:]
#amplitude and phase target 1
make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target_1), np.abs(slice_output_1)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_amplitude")
make_1Dplot_of([slice_x, slice_x], [np.angle(slice_target_1), np.angle(slice_output_1)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_phase")
#save the output fields for later use
saveFields = True
if saveFields:
np.savetxt("results/modeconv1_optimized_output_field_source1.txt", verify_out_field_1.reshape((Nx, Ny)))
np.savetxt("results/modeconv1_optimized_output_field_source2.txt", verify_out_field_2.reshape((Nx, Ny)))
np.savetxt("results/modeconv1_optimized_output_field_allSource.txt", verify_out_field_all.reshape((Nx, Ny)))
# Reshape fields to 2D for visualization
phase_mask_1 = phase_given_w(opt_weights[:S]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(opt_weights[S:]).reshape((Nx, Ny))
# Plot: Phase masks
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#save the phase masks for later use
saveMasks = True
if saveMasks:
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2)
print("All plots saved to results/ folder.")
@@ -0,0 +1,799 @@
import time
import multiprocessing
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
from smt.applications.ego import EGO, KRG
from smt.sampling_methods import LHS
from smt.design_space import (
DesignSpace,
)
import os
#check results folder exists
if not os.path.exists("results"):
os.makedirs("results")
##############################
#physics and simulation domain
wavelength = 1.55e-6
k0 = 2 * np.pi / wavelength
opt_max_eval = 3
unit_cell_pitch = 700e-9 #equivalent to spatial sampling
Nx = 2048 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d = [0, 0, 0] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target
d0 = d[0] #propagation distance: source - MS1
d1 = d[1] #propagation distance: MS1 - MS2
d2 = d[2] #propagation distance: MS2 - target
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#Source
beam_waist = 4e-6
source_spacing = 0
##############################
#Plot zoom parameters [um]
zoom_x = 350
zoom_core_diameter = 2 * 6
zoom_x_outmode = 12
zoom_y = 350
zoom_y_outmode = 50
full_view_x = size_x * 1e6 / 2
full_view_y = size_y * 1e6 / 2
##############################
#PYFFTW PLANNING SETUP
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0 = np.exp(1j * phase_factor_0)
P_0_nat = np.fft.ifftshift(P_0)
phase_factor_1 = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1 = np.exp(1j * phase_factor_1)
P_1_nat = np.fft.ifftshift(P_1)
phase_factor_2 = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2 = np.exp(1j * phase_factor_2)
P_2_nat = np.fft.ifftshift(P_2)
P_1_dagger = P_1.T.conj()
P_2_dagger = P_2.T.conj()
P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj()
##############################
def update_phase_factors():
phase_factor_0[:,:] = k0 * d[0] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0[:,:] = np.exp(1j * phase_factor_0)
P_0_nat[:,:] = np.fft.ifftshift(P_0)
phase_factor_1[:,:] = k0 * d[1] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1[:,:] = np.exp(1j * phase_factor_1)
P_1_nat[:,:] = np.fft.ifftshift(P_1)
phase_factor_2[:,:] = k0 * d[2] * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2[:,:] = np.exp(1j * phase_factor_2)
P_2_nat[:,:] = np.fft.ifftshift(P_2)
P_1_dagger[:] = P_1.T.conj()
P_2_dagger[:] = P_2.T.conj()
P_1_dagger_nat[:] = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat[:] = np.fft.ifftshift(P_2).T.conj()
##############################
def update_input_fields():
input_field_1[:] = propagate_source(source_field_1)
input_field_1_2d[:] = input_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting
#initialize input field 2 (propagated source 2 field)
input_field_2[:] = propagate_source(source_field_2)
input_field_2_2d[:] = input_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field 1 integrated intensity
input_field_1_intensity = np.sum(np.abs(input_field_1)**2)
print("Input field integrated intensity: %.4e" % input_field_1_intensity)
#input field 2 integrated intensity
input_field_2_intensity = np.sum(np.abs(input_field_2)**2)
print("Input field integrated intensity: %.4e" % input_field_2_intensity)
#make a plot and save the input field
make_2Dplot_of(input_field_1_2d, choose_quantity="amplitude", save_name="input_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(input_field_1_2d, choose_quantity="phase", save_name="input_field_1_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(input_field_2_2d, choose_quantity="amplitude", save_name="input_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(input_field_2_2d, choose_quantity="phase", save_name="input_field_2_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
##############################
##############################
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern(mode_list:list)->list:
'''Returns the fiber mode pattern for the selected n index.
Args:
mode_list: the list of the selected fiber mode indices
Returns:
The fiber mode patterns corresponding to the selected indices returned
as a list of 2D arrays with the same order as the input mode_list
'''
#check if the modes in mode_list have already been compute by checking
#the saved files in the 'results' folder. If they have, load them from the files. If not, compute them and save them to the files.
fiber_modes_input_folder = "fiber_modes"
output_patterns = []
try:
for enum_index, n in enumerate(mode_list):
#try to load mode index n
try:
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy"
pattern = np.load(pattern_file)
output_patterns.append(pattern)
print("Loaded fiber patterns for mode index: ", n)
#if mode index n was not found, compute it
except FileNotFoundError:
if not os.path.exists(fiber_modes_input_folder):
os.makedirs(fiber_modes_input_folder)
print("Computing fiber patterns for mode index: ", n)
lda = 1.55e-6
n_co = 1.4630 #core refractive index
n_cl = 1.4585 #cladding refractive index
fiber_core_diameter = 12e-6 # [m]
a = fiber_core_diameter / 2 #fiber core radius
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
b_solutions_at_V_obs = []
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
b_mode_sel = b_solutions_at_V_obs[0]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n,chi_co * r) / jv(n, chi_co * a) * np.cos(n * phi)
else:#outside core
E_field[i,j] = kv(n, chi_cl * r) / kv(n, chi_cl * a) * np.cos(n * phi)
#save the computed pattern to a file for future use
pattern_file = fiber_modes_input_folder + f"/LP{n}1_field_distribution.npy"
np.save(pattern_file, E_field)
output_patterns.append(E_field)
return output_patterns
except:
raise(Exception("Error in computing fiber patterns"))
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a 2D normalized Gaussian source field for a given beam waist and centered at the specified (x,y) coordinates.
Normalization consists in scaling the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The 2D normalized Gaussian source field.
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source(source_to_propagate):
'''Returns the propagated source field (flattened) just before metasurface 1 (MS1).
This involves propagating the source for a distance d0: from its plane of definition up to MS1.
Args:
source_to_propagate: the source field to be propagated (flattened)
Returns:
The propagated source field (flattened) just before MS1.
'''
fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((Nx, Ny)))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = np.fft.fftshift(ifft_operator.output_array).flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
plt.plot(x_axis[i], y_axis[i])
plt.xlabel("x [um]")
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def make_polarPlot_of(given_field,choose_quantity="amplitude",save_name="polar_plot",plot_zoom_r=zoom_x) -> None:
'''Makes a polar plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_r: zoom level for the radial axis (in micrometers)
Returns:
None
'''
fig, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
given_field_2d = np.abs(given_field.reshape((Nx, Ny))) #assure the field is 2D before plotting
im = ax.imshow(given_field_2d,origin='lower')
elif choose_quantity == "phase":
given_field_2d = (np.angle(given_field.reshape((Nx, Ny))) + 2*np.pi) % (2*np.pi)#assure the field is 2D before plotting
im = ax.imshow(given_field_2d, cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
center_x, center_y = Nx//2, Ny//2
radius = plot_zoom_r * 1e-6 / (size_x/2) * (Nx/2) #convert zoom in um to pixels
circle = patches.Circle((center_x, center_y), radius, transform=ax.transData)
im.set_clip_path(circle)
ax.set_xlim(center_x - radius, center_x + radius)
ax.set_ylim(center_y - radius, center_y + radius)
ax.axis('off')
cbar = plt.colorbar(im, ax=ax)
plt.savefig("results/" + save_name + ".pdf")
plt.close()
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the simulation spatial grid by a given physical (true space) amount.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X + shift_amount_x
Yp = Y + shift_amount_y
return Xp, Yp
###########################
#initialize source field
source_field_1 = get_source_field(beam_waist, center_x=-source_spacing/2, center_y=0)
source_field_2 = get_source_field(beam_waist, center_x=source_spacing/2, center_y=0)
#source field integrated intensity
source_1_power = np.sum(np.abs(source_field_1)**2)
source_2_power = np.sum(np.abs(source_field_2)**2)
print("Source 1 field integrated intensity: %.4e" % source_1_power)
print("Source 2 field integrated intensity: %.4e" % source_2_power)
#make a plot and save the source field
make_2Dplot_of(source_field_1, choose_quantity="amplitude", save_name="source_1_field_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(source_field_1, choose_quantity="phase", save_name="source_1_field_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(source_field_2, choose_quantity="amplitude", save_name="source_2_field_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(source_field_2, choose_quantity="phase", save_name="source_2_field_phase", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
###########################
###########################
#initialize input field 1 (propagated source 1 field)
input_field_1 = propagate_source(source_field_1)
input_field_1_2d = input_field_1.reshape((Nx, Ny)) #reshape to 2D for plotting
#initialize input field 2 (propagated source 2 field)
input_field_2 = propagate_source(source_field_2)
input_field_2_2d = input_field_2.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field 1 integrated intensity
input_field_1_intensity = np.sum(np.abs(input_field_1)**2)
print("Input field integrated intensity: %.4e" % input_field_1_intensity)
#input field 2 integrated intensity
input_field_2_intensity = np.sum(np.abs(input_field_2)**2)
print("Input field integrated intensity: %.4e" % input_field_2_intensity)
#make a plot and save the input field
#make_2Dplot_of(input_field_1_2d, choose_quantity="amplitude", save_name="input_field_1_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make_2Dplot_of(input_field_1_2d, choose_quantity="phase", save_name="input_field_1_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make_2Dplot_of(input_field_2_2d, choose_quantity="amplitude", save_name="input_field_2_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make_2Dplot_of(input_field_2_2d, choose_quantity="phase", save_name="input_field_2_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#list of input fields to be propagated within the optimization loop
input_field_list = [input_field_1, input_field_2]
###########################
###########################
#initialize target field
target_mode_indices = [2]
fiber_mode_pattern_1, fiber_mode_pattern_2 = get_fiber_mode_pattern(target_mode_indices)
target_Efield_1_2d = fiber_mode_pattern_1 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_1)**2))
target_Efield_1 = target_Efield_1_2d.flatten() #normalized target field (intensity = 1)
target_Efield_2_2d = fiber_mode_pattern_2 / np.sqrt(np.sum(np.abs(fiber_mode_pattern_2)**2))
target_Efield_2 = target_Efield_2_2d.flatten() #normalized target field (intensity = 1)
# Plot target fields using polar coordinates
make_polarPlot_of(target_Efield_1_2d, choose_quantity="amplitude", save_name="target_field_1_amplitude_polar", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(target_Efield_1_2d, choose_quantity="phase", save_name="target_field_1_phase_polar", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(target_Efield_2_2d, choose_quantity="amplitude", save_name="target_field_2_amplitude_polar", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(target_Efield_2_2d, choose_quantity="phase", save_name="target_field_2_phase_polar", plot_zoom_r=zoom_core_diameter)
target_field_list = [target_Efield_1, target_Efield_2]
###########################
def forward_propagate(input_field:np.ndarray,weights:np.ndarray) -> np.ndarray:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
the output field computed at the observation plane
'''
print("Starting propagating...")
intermediate_fields = [None, None]
start_time = time.time()
phase_mask = phase_given_w(weights)
fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)))
fft_operator()
#print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
#FIRST PROPAGATION
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
#print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
#print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy())
#print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2))
fft_input_array[:,:] = np.fft.ifftshift((np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny)))))
fft_operator()
#SECOND PROPAGATION
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy())
end_time = time.time()
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
return intermediate_fields
def adjoint_propagate(input_field_2d,target_Efield_2d,intermediate_fields,input_weights):
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradients of the cost function with respect to the phase mask parameters (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
phase_mask = phase_given_w(input_weights)
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d)
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger)
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def value_and_grad(weights):
'''
Returns the value of the cost function and its gradient with respect to the phase mask parameters for
every available set of input/target fields.
Args:
weights: the current phase mask parameters (flattened)
Returns:
values: the value of the cost function for each input/target field pair
gradients: the gradient of the cost function with respect to the phase mask parameters for each input/target field pair
'''
values = []
gradients = []
phase_mask[:] = phase_given_w(weights) #update phase mask in place
for i, input_field in enumerate(input_field_list):
intermediate_fields = forward_propagate(input_field, weights)
#circular_mask = np.where(rho.flatten() < 10*6e-6, 1, 0)
C_s = np.abs(np.sum(intermediate_fields[-1].flatten() * np.conj(target_field_list[i]))) ** 2
C = np.real(np.sum(C_s))
values.append(C)
gradients.append(
adjoint_propagate(
input_field_2d = input_field.reshape((Nx, Ny)),
target_Efield_2d = target_field_list[i].reshape((Nx, Ny)),
intermediate_fields = intermediate_fields,
input_weights = weights
)
)
return np.array(values), np.array(gradients)
opt_history = []
opt_history1 = []
opt_history2 = []
def obj_fun(weights, grad):
'''
Objective function for the optimization. Computes the overall cost function by taking the mean value
of the individual cost functions for each input/target field pair. Also computes the gradient of the overall cost function
with respect to the weights parameters.
Args:
weights: the current normalized phase mask parameters (flattened)
grad: the gradient of the overall cost function with respect to the weights parameters (flattened)
Returns:
average_obj_val: the average value of the cost function across all input/target field pairs
'''
w_norm[:] = weights
obj_val, grads = value_and_grad(weights)
average_obj_val = np.mean(obj_val)
if grads.size > 0:
target_num = len(target_field_list)
if target_num > 1:
grads_sum = grads[0] + grads[1]
else:
grads_sum = grads[0]
grad[:] = 1 / target_num * (grads_sum) * 2 * np.pi
#make a plot of the magnitude of the gradient
#make_2Dplot_of(np.abs(grad[S:].reshape((Nx, Ny))), choose_quantity="amplitude", save_name="gradient_magnitude", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
opt_history.append(average_obj_val)
opt_history1.append(obj_val[0])
opt_history2.append(obj_val[1])
make_1Dplot_of([np.arange(len(opt_history))]*3, [opt_history,opt_history1,opt_history2], plot_zoom_x=len(opt_history), save_name="optimization_history")
return average_obj_val
def initialize_masks():
X1,Y1 = shift_spatial_grid(-source_spacing/2,0)
rho1 = np.sqrt(X1**2 + Y1**2)
circular_mask_1 = np.where(rho1.flatten() < 3*beam_waist, 1, 0)
X2,Y2 = shift_spatial_grid(source_spacing/2,0)
rho2 = np.sqrt(X2**2 + Y2**2)
circular_mask_2 = np.where(rho2.flatten() < 3*beam_waist, 1, 0)
target_pattern_1_norm_phase = (np.angle(target_Efield_1) + 2 * np.pi) % (2 * np.pi) / (2*np.pi)
target_pattern_1_norm_phase = np.roll(target_pattern_1_norm_phase, shift=int(source_spacing/sampling_period/2), axis=0)
target_pattern_2_norm_phase = (np.angle(target_Efield_2) + 2 * np.pi) % (2 * np.pi) / (2*np.pi)
target_pattern_2_norm_phase = np.roll(target_pattern_2_norm_phase, shift=-int(source_spacing/sampling_period/2), axis=0)
#on metasurface 1, set initial guess on normalized phase elements
w_norm[:S] += circular_mask_1 * target_pattern_1_norm_phase + circular_mask_2 * target_pattern_2_norm_phase
#global definitions
w_norm = np.zeros((2*S))
phase_mask = np.zeros((2*S))
def reset_globals():
#reset global variables before next EGO iteration
opt_history[:] = []
opt_history1[:] = []
opt_history2[:] = []
w_norm[:] = np.zeros((2*S))
phase_mask[:] = np.zeros((2*S))
def metaprop_solve():
###########################
#initialize the normalized phase masks and global phase mask
initialize_masks()
phase_mask[:] = phase_given_w(w_norm)
#make an initial plot of the phase masks
#make_2Dplot_of(phase_mask[:S].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#make_2Dplot_of(phase_mask[S:].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
norm_phase_min = 0.0
norm_phase_max = 1.0
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
###########################
#initialize nlopt solver
solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S) #weights (2S) + epigraph (1)
weights_lower_bounds = mask_lower_bounds.flatten()
weights_upper_bounds = mask_upper_bounds.flatten()
solver.set_lower_bounds(weights_lower_bounds)
solver.set_upper_bounds(weights_upper_bounds)
solver.set_max_objective(obj_fun)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-7)
solver.set_param("verbosity",1)
print("Starting optimization...")
start = True
if start:
w_norm[:] = solver.optimize(w_norm)
print("Optimization completed.")
#verify the results
opt_weights = w_norm
if len(input_field_list) > 1:
verify_out_field_1 = forward_propagate(input_field_list[0], opt_weights)[-1]
verify_out_field_2 = forward_propagate(input_field_list[1], opt_weights)[-1]
make_polarPlot_of(verify_out_field_1, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source1_amplitude", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_1, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source1_phase", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_2, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source2_amplitude", plot_zoom_r=zoom_core_diameter)
make_polarPlot_of(verify_out_field_2, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source2_phase", plot_zoom_r=zoom_core_diameter)
#make 1D slices of target/output fields to compare
slice_y_index = Ny//2
slice_x = xs * 1e6
slice_target_1 = target_Efield_1.reshape((Nx, Ny))[slice_y_index,:]
slice_target_2 = target_Efield_2.reshape((Nx, Ny))[slice_y_index,:]
slice_output_1 = verify_out_field_1.reshape((Nx, Ny))[slice_y_index,:]
slice_output_2 = verify_out_field_2.reshape((Nx, Ny))[slice_y_index,:]
#amplitude and phase target 1
make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target_1), np.abs(slice_output_1)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_amplitude")
make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target_1)+2*np.pi)%(2*np.pi), (np.angle(slice_output_1)+2*np.pi)%(2*np.pi)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_phase")
#amplitude and phase target 2
make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target_2), np.abs(slice_output_2)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source2_amplitude")
make_1Dplot_of([slice_x, slice_x], [(np.angle(slice_target_2)+2*np.pi)%(2*np.pi), (np.angle(slice_output_2)+2*np.pi)%(2*np.pi)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source2_phase")
else:
verify_out_field_1 = forward_propagate(input_field_list[0], opt_weights)[-1]
make_2Dplot_of(verify_out_field_1, choose_quantity="amplitude", save_name="modeconv1_optimized_output_field_source1_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(verify_out_field_1, choose_quantity="phase", save_name="modeconv1_optimized_output_field_source1_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
#make a slice of the target/output fields to compare
slice_y_index = Ny//2
slice_x = xs * 1e6
slice_target_1 = target_Efield_1.reshape((Nx, Ny))[slice_y_index,:]
slice_output_1 = verify_out_field_1.reshape((Nx, Ny))[slice_y_index,:]
#amplitude and phase target 1
make_1Dplot_of([slice_x, slice_x], [np.abs(slice_target_1), np.abs(slice_output_1)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_amplitude")
make_1Dplot_of([slice_x, slice_x], [np.angle(slice_target_1), np.angle(slice_output_1)], plot_zoom_x=zoom_x_outmode, save_name="modeconv1_output_vs_target_source1_phase")
#save the output fields for later use
saveFields = False
if saveFields:
np.savetxt("results/modeconv1_optimized_output_field_source1.txt", verify_out_field_1.reshape((Nx, Ny)))
np.savetxt("results/modeconv1_optimized_output_field_source2.txt", verify_out_field_2.reshape((Nx, Ny)))
# Reshape fields to 2D for visualization
phase_mask_1 = phase_given_w(opt_weights[:S]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(opt_weights[S:]).reshape((Nx, Ny))
# Plot: Phase masks
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=full_view_x, plot_zoom_y=full_view_y)
#save the phase masks for later use
saveMasks = False
if saveMasks:
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2)
print("All plots saved to results/ folder.")
def metaprop(x):
if not os.path.exists('results/metaprop_results.txt'):
with open('results/metaprop_results.txt', 'w') as f:
f.write("d1 [m] d2 [m] d3 [m] ob1 ob2 t\n")
m, n = x.shape
C = np.zeros((m,1))
for i in range(m):
for j in range(n):
d[j] = 1e-6*np.round(x[i,j]) #update the global propagation distances with the new design parameters
d[-1] = d[0]
print("Current propagation distances: ", d)
update_phase_factors() #update the phase factors for the new propagation distances
update_input_fields()
print("USING D=",d)
metaprop_solve() #solve the inner optimization problem for the given design parameters
ob1 = opt_history1[-1]
ob2 = opt_history2[-1]
avg = opt_history[-1]
#save all the partial results for a specific set of design parameters x to a text file
#in append mode, so that we can keep track of the optimization history
with open('results/metaprop_results.txt', 'a') as f:
f.write(f"{d[0]:.6e} {d[1]:.6e} {d[2]:.6e} {ob1:.6e} {ob2:.6e} {avg:.6e}\n")
alpha = 0.5
C[i] = -1*avg + alpha * np.abs(ob1 - ob2) #cost function to be minimized by EGO
reset_globals()
return C
def run_ego():
##############################
#EGO setup
##############################
n_ego_iter = 3
xlimits = np.array([[200,700], [200,700]])
seed = 777
design_space = DesignSpace(xlimits, seed=seed)
criterion = 'EI'
ndoe = 2
#build DOE
sampling = LHS(xlimits=xlimits, seed=seed)
xdoe = sampling(ndoe)
sm = KRG(design_space=design_space, n_start=25, print_global=False)
ego = EGO(
n_iter=n_ego_iter,
criterion=criterion,
xdoe=xdoe,
surrogate=sm,
n_start=25,
)
x_opt, y_opt, ind_best, x_data, y_data = ego.optimize(fun=metaprop)
#save the optimization results to a text file
np.savetxt('results/ego_opt_results.txt', np.hstack((x_opt,y_opt)))
if __name__ == "__main__":
run_ego()
@@ -0,0 +1,611 @@
import time
import multiprocessing
import pyfftw
#pyFFTW setup
n_cpu = multiprocessing.cpu_count()
pyfftw.config.NUM_THREADS = n_cpu
pyfftw.interfaces.cache.enable()
pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE'
import nlopt
import numpy as np
import matplotlib.pyplot as plt
#set the global font size for all plots
plt.rcParams.update({'font.size': 14})
from scipy.special import jv, kv
##############################
#physics and simulation domain
wavelength = 1.55e-6
k0 = 2 * np.pi / wavelength
opt_max_eval = 250
unit_cell_pitch = 400e-9 #equivalent to spatial sampling
Nx = 2048 #pixels per dimension
Ny = Nx
S = Nx * Ny
size_x = unit_cell_pitch * Nx #actual physical size
size_y = unit_cell_pitch * Ny
xs = np.linspace(-size_x/2, size_x/2 - size_x / Nx, Nx)
ys = np.linspace(-size_y/2, size_y/2 - size_y / Ny, Ny)
X, Y = np.meshgrid(xs, ys)
rho = np.sqrt(X**2 + Y**2)
sampling_period = xs[1] - xs[0]
d0 = 300e-6 #propagation distance: source - MS1
d1 = 500e-6 #propagation distance: MS1 - MS2
d2 = 300e-6 #propagation distance: MS2 - target
d = [d1, d2] #d = [d1,d2] d1:distance MS1-MS2, d2: distance MS2-target
phase_min = 0
phase_max = 2 * np.pi
##############################
##############################
#Source
beam_waist = 4e-6
source_spacing = 300e-6
##############################
##############################
#plan pyfftw objects for forward and inverse FFTs
fft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_input_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
ifft_output_array = pyfftw.empty_aligned((Nx, Ny), dtype='complex128', n=16)
fft_operator = pyfftw.FFTW(fft_input_array, fft_output_array, axes=(0,1), direction='FFTW_FORWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
ifft_operator = pyfftw.FFTW(ifft_input_array, ifft_output_array, axes=(0,1), direction='FFTW_BACKWARD', threads=n_cpu, flags=('FFTW_MEASURE','FFTW_DESTROY_INPUT',))
##############################
##############################
#setting up spatial frequencies
ks = 2 * np.pi / sampling_period
kappas = np.arange(-Nx//2, Nx//2) * ks / Nx
KX, KY = np.meshgrid(kappas, kappas)
K_parallel = np.sqrt(KX**2 + KY**2)
nu_parallel = K_parallel / (2 * np.pi)
phase_factor_0 = k0 * d0 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_0 = np.exp(1j * phase_factor_0)
P_0_nat = np.fft.ifftshift(P_0)
phase_factor_1 = k0 * d1 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_1 = np.exp(1j * phase_factor_1)
P_1_nat = np.fft.ifftshift(P_1)
phase_factor_2 = k0 * d2 * np.sqrt(1 - (wavelength * nu_parallel) ** 2 + 0*1j)
P_2 = np.exp(1j * phase_factor_2)
P_2_nat = np.fft.ifftshift(P_2)
P_1_dagger = P_1.T.conj()
P_2_dagger = P_2.T.conj()
P_2_conj = P_2.conj()
P_1_dagger_nat = np.fft.ifftshift(P_1).T.conj()
P_2_dagger_nat = np.fft.ifftshift(P_2).T.conj()
##############################
##############################
#Plot zoom parameters [um]
zoom_x = 200
zoom_y = 200
all_view_x = size_x * 1e6 / 2
all_view_y = size_y * 1e6 / 2
##############################
def phase_given_w(w):
'''Returns the phase shift given the design parameters w.
Args:
w: design parameters (flattened)
Returns:
The phase shift corresponding to the geometrical parameters w (flattened)'''
#for simplicity, we linearly map w (0,1) to a phase shift (0, 2pi)
return 2 * np.pi * (w)
def get_pattern() -> np.ndarray:
'''Returns the flattened target fiber mode pattern'''
pattern_name = 'LP11_field_distribution.txt'
pattern = np.loadtxt('misc/' + pattern_name, dtype=complex)
return pattern.flatten()
def get_fiber_mode_pattern():
'''Returns the target fiber mode pattern'''
print("Computing target fiber mode pattern...")
lda = 1.55e-6
n_co = 1.4630
n_cl = 1.4585
a = 6e-6
#find the v parameter range that corresponds to the 1550 nm wavelength
#v_1550 = 2 * np.pi * a / lda * np.sqrt(n_co**2 - n_cl**2)
v_1550 = 4.5
b = np.linspace(1e-3, 1-1e-3, 400000)
V_list = np.linspace(2, 5.1, 20)
V_obs = v_1550
#b, V = np.meshgrid(b_list, V_list)
n_list = [0,1,2]
b_solutions_at_V_obs = []
for n in n_list:
b_solutions = []
for V in V_list:
LHS = np.sqrt(1-b) * jv(n+1, V * np.sqrt(1-b)) / jv(n, V * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V * np.sqrt(b)) / kv(n, V * np.sqrt(b))
intersection_index = np.argmax(np.where(np.isclose(LHS, RHS, atol=0.000001),1,0).flatten())
b_solutions.append(b[intersection_index])
LHS = np.sqrt(1-b) * jv(n+1, V_obs * np.sqrt(1-b)) / jv(n, V_obs * np.sqrt(1-b))
RHS = np.sqrt(b) * kv(n+1, V_obs * np.sqrt(b)) / kv(n, V_obs * np.sqrt(b))
b_solutions_at_V_obs.append(b[np.argmax(np.where(np.isclose(LHS, RHS, atol=0.00001),1,0).flatten())])
n_mode_sel = 2
b_mode_sel = b_solutions_at_V_obs[n_mode_sel]
k_co = 2 * np.pi / lda * n_co
k_cl = 2 * np.pi / lda * n_cl
beta = np.sqrt(b_mode_sel * (k_co**2 - k_cl**2) + k_cl**2)
chi_co = v_1550 * np.sqrt(1 - b_mode_sel) / a
chi_cl = v_1550 * np.sqrt(b_mode_sel) / a
E_field = np.zeros_like(X, dtype=complex)
for i in range(Nx):
for j in range(Ny):
r = np.sqrt(X[i,j]**2 + Y[i,j]**2)
if r < 10*a:
phi = np.arctan2(Y[i,j], X[i,j])
if r < a:#inside core
E_field[i,j] = jv(n_mode_sel,chi_co * r) / jv(n_mode_sel, chi_co * a) * np.cos(n_mode_sel * phi)
else:#outside core
E_field[i,j] = kv(n_mode_sel, chi_cl * r) / kv(n_mode_sel, chi_cl * a) * np.cos(n_mode_sel * phi)
return E_field
def shift_spatial_grid(shift_amount_x,shift_amount_y):
'''Shifts the spatial grid by a given amount. This is useful for centering the source field on the metasurface.
Args:
shift_amount_x: amount to shift the grid in x direction (in meters)
shift_amount_y: amount to shift the grid in y direction (in meters)
Returns:
the shifted spatial grid (X, Y)'''
Xp = X - shift_amount_x
Yp = Y - shift_amount_y
return Xp, Yp
def get_source_field(beam_waist,center_x=0,center_y=0,):
'''Returns a normalized Gaussian source field for a given beam waist. Normalization consists in scaling
the field such that its integrated intensity is 1.
Args:
beam_waist: the waist of the Gaussian beam (in meters)
center_x: the x-coordinate of the center of the Gaussian beam (in meters)
center_y: the y-coordinate of the center of the Gaussian beam (in meters)
Returns:
The normalized Gaussian source field
'''
if center_x != 0 or center_y != 0:
X_shifted, Y_shifted = shift_spatial_grid(center_x, center_y)
rho = np.sqrt(X_shifted**2 + Y_shifted**2)
else:
rho = np.sqrt(X**2 + Y**2)
gaussian_field = np.exp(-(rho)**2 / (beam_waist)**2) #Gaussian input field, flattened
gaussian_field_normalized = (gaussian_field / np.sqrt(np.sum(np.abs(gaussian_field)**2))) #normalize the input field to have power = 1
return gaussian_field_normalized
def propagate_source():
'''Returns the propagated source field just before metasurface 1 (MS1). This involves propagating the
source for a distance d0: from its plane of definition up to MS1.
Returns:
The propagated source field just before MS1 (flattened)
'''
fft_input_array[:,:] = source_field_2d
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_0_nat
ifft_operator()
propagated_source_field = ifft_operator.output_array.flatten()
return propagated_source_field
def make_1Dplot_of(x_axis,y_axis,plot_zoom_x=zoom_x,save_name="plot_1D",) -> None:
'''Makes a 1D plot of the given data and saves it to a PDF file.
Args:
x_axis: list of x-axis data (list of 1D arrays)
y_axis: list of y-axis data (list of 1D arrays)
plot_zoom_x: zoom level for the x-axis (in micrometers)
save_name: name of the file to save the plot (without extension)
Returns:
None
'''
if len(x_axis) != len(y_axis):
raise ValueError("Lists x_axis and y_axis must have the same length.")
plt.figure()
for i in range(len(x_axis)):
plt.plot(x_axis[i], y_axis[i])
plt.xlabel("x [um]")
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.close()
def make_2Dplot_of(given_field,choose_quantity="amplitude",save_name="plot",plot_zoom_x=zoom_x,plot_zoom_y=zoom_y) -> None:
'''Makes a plot of the given 2D field in a zoomed region.
Saves the plot with the chosen filename to a PDF file.
Args:
given_field: the 2D field to be plotted (flattened)
choose_quantity: either "amplitude" or "phase" to select quantity to plot
save_name: name of the file to save the plot (without extension)
plot_zoom_x: zoom level for the x-axis (in micrometers)
plot_zoom_y: zoom level for the y-axis (in micrometers)
Returns:
None'''
given_field_2d = given_field.reshape((Nx, Ny)) #assure the field is 2D before plotting
plt.figure()
if choose_quantity == "amplitude":
plt.imshow(np.abs(given_field_2d), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2))
elif choose_quantity == "phase":
plt.imshow((np.angle(given_field_2d)+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
elif choose_quantity == "phase_mask":
plt.imshow((given_field_2d+2*np.pi)%(2*np.pi), extent=(-size_x*1e6/2, size_x*1e6/2, -size_y*1e6/2, size_y*1e6/2), cmap='hsv', vmin=0, vmax=2*np.pi)
else:
raise ValueError("choose_quantity must be either 'amplitude' or 'phase'")
plt.xlim(-plot_zoom_x, plot_zoom_x)
plt.ylim(-plot_zoom_y, plot_zoom_y)
plt.xlabel("x [um]")
plt.ylabel("y [um]")
plt.colorbar()
plt.tight_layout()
plt.savefig("results/" + save_name + ".pdf")
plt.close()
###########################
#initialize source field
source_field = get_source_field(beam_waist,center_x=source_spacing/2,center_y=0)
source_field_2d = source_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#source field integrated intensity
source_power = np.sum(np.abs(source_field)**2)
print("Source field integrated intensity: %.4e" % source_power)
#make a plot and save the source field
make_2Dplot_of(source_field_2d, choose_quantity="amplitude", save_name="source_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(source_field_2d, choose_quantity="phase", save_name="source_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
###########################
###########################
#initialize input field (propagated source field)
input_field = propagate_source()
input_field_2d = input_field.reshape((Nx, Ny)) #reshape to 2D for plotting
#input field integrated intensity
input_field_intensity = np.sum(np.abs(input_field)**2)
print("Input field integrated intensity: %.4e" % input_field_intensity)
#make a plot and save the input field
make_2Dplot_of(input_field_2d, choose_quantity="amplitude", save_name="input_field_amplitude", plot_zoom_x=zoom_x*2, plot_zoom_y=zoom_y*2)
make_2Dplot_of(input_field_2d, choose_quantity="phase", save_name="input_field_phase", plot_zoom_x=zoom_x*2, plot_zoom_y=zoom_y*2)
###########################
###########################
#initialize target field
fiber_mode_pattern = get_fiber_mode_pattern()
target_Efield_2d = fiber_mode_pattern / np.sqrt(np.sum(np.abs(fiber_mode_pattern)**2))
#shift the target field to math the source position
#target_Efield_2d = np.roll(target_Efield_2d, shift=int((source_spacing/2/unit_cell_pitch)), axis=1) #shift the target field to match the source position
target_Efield = target_Efield_2d.flatten() #normalized target field (intensity = 1)
# Plot: Target field
make_2Dplot_of(target_Efield_2d, choose_quantity="amplitude", save_name="target_field_amplitude", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
make_2Dplot_of(target_Efield_2d, choose_quantity="phase", save_name="target_field_phase", plot_zoom_x=zoom_x, plot_zoom_y=zoom_y)
###########################
###########################
#initialize the normalized phase masks and global phase mask
w_norm = np.zeros((2*S)) #concatenated normalized parameters for both masks
X, Y = np.meshgrid(np.linspace(-size_x/2, size_x/2, Nx), np.linspace(-size_y/2, size_y/2, Ny))
rho = np.sqrt(X**2 + Y**2)
norm_phase_min = 0
norm_phase_max = 1
mask_buffer = 100e-6
mask_upper_bounds = norm_phase_max * np.where((np.abs(X.flatten()) > size_x/2 - mask_buffer) | (np.abs(Y.flatten()) > size_y/2 - mask_buffer), 0, 1)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds)
def backward_propagate(field_to_backpropagate)-> np.ndarray:
''' Backpropagates the target field up to the second metasurface.
Args:
field_to_backpropagate: the field to be backpropagated (flattened)
Returns:
The backpropagated field at the second metasurface (flattened)
'''
fft_input_array[:,:] = field_to_backpropagate.reshape((Nx, Ny))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
backpropagated_field = ifft_operator.output_array.flatten()
return backpropagated_field
def initialize_phase_masks():
'''Initializes the normalized weights, in-place.'''
initialize = True
if initialize:
#MS1
#the first metasurface should steer light towards the center of the second metasurface
#steer_angle = np.atan2(source_spacing/2,d1)
steer_angle = 0.0001
grating_period = wavelength / np.sin(steer_angle)
grating_phase = (2*np.pi - (2 * np.pi / grating_period) * X) % (2 * np.pi) #blazed grating phase
w_norm[:S] = grating_phase.flatten() / (2 * np.pi) #normalized grating phase
#MS2
#propagate the source up to just before the second metasurface
field_before_MS1 = input_field
field_after_MS1 = field_before_MS1 * np.exp(1j * phase_given_w(w_norm[:S])) #apply the first phase mask
fft_input_array[:,:] = field_after_MS1.reshape((Nx, Ny))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
ifft_operator()
field_before_MS2 = ifft_operator.output_array.flatten() #field just before the second metasurface
#make a plot of the field before the second metasurface
make_2Dplot_of(field_before_MS2, choose_quantity="amplitude", save_name="field_before_MS2_amplitude", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y)
make_2Dplot_of(field_before_MS2, choose_quantity="phase", save_name="field_before_MS2_phase", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y)
backpropagated_target_field = backward_propagate(target_Efield)
#make plots of the backpropagated target field
make_2Dplot_of(backpropagated_target_field, choose_quantity="amplitude", save_name="backpropagated_target_field_amplitude", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y)
make_2Dplot_of(backpropagated_target_field, choose_quantity="phase", save_name="backpropagated_target_field_phase", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y)
#MS2_phase = (np.angle(backpropagated_target_field) + 2 * np.pi) % (2 * np.pi) - (np.angle(field_before_MS2) + 2 * np.pi) % (2 * np.pi)
#MS2_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi) - (np.angle(field_before_MS2) + 2 * np.pi) % (2 * np.pi)
MS2_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi)
MS2_phase = (MS2_phase + 2 * np.pi) % (2 * np.pi) #make sure the phase is between 0 and 2pi
#normalize phase masks
circular_mask = np.where(rho.flatten() < 20*6e-6, 1, 0)
w_norm[:S] = 0*circular_mask * grating_phase.flatten() / (2 * np.pi)
w_norm[S:] = circular_mask * MS2_phase.flatten() / (2 * np.pi)
#target_pattern_phase = (np.angle(target_Efield_2d) + 2 * np.pi) % (2 * np.pi) #make sure the phase is between 0 and 2pi
#target_pattern_phase = np.roll(target_pattern_phase, shift=int((source_spacing/2/unit_cell_pitch)), axis=1) #shift the target phase to match the source position
#initialize phase masks
initialize_phase_masks()
phase_mask = phase_given_w(w_norm)
make_2Dplot_of(phase_mask[:S].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_1", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y)
make_2Dplot_of(phase_mask[S:].reshape((Nx, Ny)), choose_quantity="phase_mask", save_name="initial_phase_mask_2", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y)
#define bounding box for the optimization. The outer mask is fixed to zero phase, while the inner mask can vary between 0 and 2pi phase.
#the bounding box is the outer shell of a square with thickness 100 um
###########################
intermediate_fields = [None, None]
def forward_propagate() -> None:
'''Propagates the input field through the system. Stores results in-place within the intermediate_fields list.
Returns:
None
'''
print("Starting propagating...")
start_time = time.time()
fft_input_array[:,:] = np.fft.ifftshift((input_field * np.exp(1j * phase_mask[:S])).reshape((Nx, Ny)))
fft_operator()
print("BEFORE")
fft_power = np.sum(np.abs(fft_operator.output_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(fft_operator.output_array * non_propagating_mask)**2)/S
print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_input_array[:,:] = fft_operator.output_array * P_1_nat #element wise product
print("AFTER")
fft_power = np.sum(np.abs(ifft_operator.input_array)**2)/S
non_propagating_mask = np.where((wavelength * nu_parallel)**2 > 1, 0, 1)
non_propagating_mask = np.fft.ifftshift(non_propagating_mask)
propagating_power = np.sum(np.abs(ifft_operator.input_array * non_propagating_mask)**2)/S
print("FFT power: %.4e, Propagating power: %.4e, Non-propagating power: %.4e" % (fft_power, propagating_power, fft_power - propagating_power))
ifft_operator()
intermediate_fields[0] = np.fft.fftshift(ifft_operator.output_array.copy())
print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(intermediate_fields[0])**2))
fft_input_array[:,:] = np.fft.ifftshift((np.fft.fftshift(ifft_operator.output_array) * np.exp(1j * phase_mask[S:].reshape((Nx, Ny)))))
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_nat #element wise product
ifft_operator()
intermediate_fields[1] = np.fft.fftshift(ifft_operator.output_array.copy())
end_time = time.time()
#plot intermediate fields
make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_1_amplitude", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y)
make_2Dplot_of(intermediate_fields[0], choose_quantity="phase", save_name="intermediate_field_1_phase", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y)
make_2Dplot_of(intermediate_fields[1], choose_quantity="amplitude", save_name="intermediate_field_2_amplitude", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y)
make_2Dplot_of(intermediate_fields[1], choose_quantity="phase", save_name="intermediate_field_2_phase", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y)
print("Propagation finished in {:.6f} seconds.".format(end_time - start_time))
def adjoint_propagate():
'''Backpropagates the output field using the adjoint of the propagation matrix P.
Returns:
the gradient of the cost function with respect to the phase mask (flattened)
'''
print("Computing adjoint...")
start_time = time.time()
Phi_1_dagger = np.exp(-1j * phase_mask[:S]).reshape((Nx, Ny))
Phi_2_dagger = np.exp(-1j * phase_mask[S:]).reshape((Nx, Ny))
#compute the adjoint source
fft_input_array[:,:] = np.fft.ifftshift(np.sum(intermediate_fields[1] * np.conj(target_Efield_2d)) * target_Efield_2d)
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_2_dagger_nat
ifft_operator()
fft_input_array[:,:] = np.fft.ifftshift(np.fft.fftshift(ifft_operator.output_array) * Phi_2_dagger)
grad_C_phi_2 = 2 * np.real(-1j * intermediate_fields[0].conj() * np.fft.fftshift(fft_input_array)).flatten()
fft_operator()
ifft_input_array[:,:] = fft_operator.output_array * P_1_dagger_nat #element wise
ifft_operator()
grad_C_phi_1 = 2 * np.real(-1j * input_field_2d.conj() * np.fft.fftshift(ifft_operator.output_array) * Phi_1_dagger).flatten()
end_time = time.time()
print("Adjoint finished in {:.6f} seconds.".format(end_time - start_time))
return np.concatenate((grad_C_phi_1, grad_C_phi_2))
def _compute_cost():
forward_propagate()
output_field = intermediate_fields[-1].flatten()
#circular_mask = np.where(rho.flatten() < 20*6e-6, 1, 0)
C_s = np.abs(np.sum(output_field * np.conj(target_Efield))) ** 2
C_s = np.real(np.sum(C_s))
C_t = C_s
return C_t
opt_history = []
def cost_fun(x, grad):
''' Cost function to be minimized.
Args:
x: input normalized parameter field (flattened)
grad: gradient of the cost function with respect to x, modified in place (flattened)
Returns:
The cost function value for the input parameter field x.
'''
w_norm[:] = x
phase_mask[:] = phase_given_w(w_norm)
C = _compute_cost()
opt_history.append(C)
plt.figure()
plt.plot(opt_history)
plt.xlabel('Iteration')
plt.ylabel('Cost Function Value')
plt.title('Optimization History')
plt.tight_layout()
plt.savefig("results/modeconv1_optimization_history.pdf")
plt.close()
if grad.size > 0:
grad[:] = adjoint_propagate() * 2 * np.pi
#print the gradient magnitude
grad_magnitude = np.abs(grad[S:]).reshape((Nx, Ny))
make_2Dplot_of(grad_magnitude, choose_quantity="amplitude", save_name="gradient_magnitude", plot_zoom_x=all_view_x, plot_zoom_y=all_view_y)
return C
if __name__ == "__main__":
#initialize nlopt solver
solver = nlopt.opt(nlopt.LD_CCSAQ, 2*S)
solver.set_lower_bounds(mask_lower_bounds.flatten())
solver.set_upper_bounds(mask_upper_bounds.flatten())
solver.set_max_objective(cost_fun)
solver.set_maxeval(opt_max_eval)
solver.set_param("dual_ftol_rel", 1e-7)
solver.set_param("verbosity",1)
print("Starting optimization...")
start = True
if start:
w_norm[:] = solver.optimize(w_norm)
print("Optimization completed.")
#verify the results
phase_mask = phase_given_w(w_norm)
forward_propagate()
output_field = intermediate_fields[-1]
input_power = np.sum(np.abs(input_field)**2)
output_power = np.sum(np.abs(output_field)**2)
print("Norm. input power: %.4e" % input_power)
print("Norm. output power: %.4e" % output_power)
#save the output field for later use
np.savetxt("results/modeconv1_optimized_output_field.txt", output_field.reshape((Nx, Ny)))
# Plot results in individual PDF figures
# Reshape fields to 2D for visualization
output_field_2d = output_field.reshape((Nx, Ny))
phase_mask_1 = phase_given_w(w_norm[:S]).reshape((Nx, Ny))
phase_mask_2 = phase_given_w(w_norm[S:]).reshape((Nx, Ny))
a = 6e-6
d = 10 * a * 1e6
# Plot: Output field amplitude and phase
make_2Dplot_of(output_field_2d, choose_quantity="amplitude", save_name="output_field_amplitude", plot_zoom_x=d, plot_zoom_y=d)
make_2Dplot_of(output_field_2d, choose_quantity="phase", save_name="output_field_phase", plot_zoom_x=d, plot_zoom_y=d)
phase_mask_zoom = 150
# Plot: Phase mask 1
make_2Dplot_of(phase_mask_1, choose_quantity="phase_mask", save_name="phase_mask_1", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
# Plot: Phase mask 2
make_2Dplot_of(phase_mask_2, choose_quantity="phase_mask", save_name="phase_mask_2", plot_zoom_x=phase_mask_zoom, plot_zoom_y=phase_mask_zoom)
# Plot: Cuts along y=0 axis - Amplitude comparison
y_center_idx = Ny // 2
output_amplitude_cut = np.abs(output_field_2d[y_center_idx, :]) / np.max(np.abs(output_field_2d[y_center_idx, :]))
target_amplitude_cut = np.abs(target_Efield_2d[y_center_idx, :]) / np.max(np.abs(target_Efield_2d[y_center_idx, :]))
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(xs*1e6, output_amplitude_cut, label='Output Field', linewidth=2)
ax.plot(xs*1e6, target_amplitude_cut, label='Target Field', linewidth=2)
ax.set_xlabel('x [um]')
ax.set_xlim(-d,d)
ax.set_ylabel('Amplitude')
ax.set_title('Norm. Field Amplitude along y=0 axis')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("results/amplitude_cut_comparison.pdf")
plt.close()
# Plot 8: Cuts along y=0 axis - Phase comparison
output_phase_cut = (np.angle(output_field_2d[y_center_idx, :]) + 2*np.pi) % (2*np.pi)
target_phase_cut = (np.angle(target_Efield_2d[y_center_idx, :]) + 2*np.pi) % (2*np.pi)
x_ax = xs*1e6
make_1Dplot_of([x_ax, x_ax], [output_phase_cut, target_phase_cut], save_name="phase_cut_comparison")
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(xs*1e6, output_phase_cut, label='Output Field', linewidth=2)
ax.plot(xs*1e6, target_phase_cut, label='Target Field', linewidth=2)
ax.set_xlabel('x [um]')
ax.set_ylabel('Phase (rad)')
ax.set_xlim(-d,d)
ax.set_title('Field Phase along y=0 axis')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("results/phase_cut_comparison.pdf")
plt.close()
# Plot 9: Intermediate field before second phase mask
make_2Dplot_of(intermediate_fields[0], choose_quantity="amplitude", save_name="intermediate_field_amplitude", plot_zoom_x=d, plot_zoom_y=d)
#save the phase masks for later use
np.savetxt("results/optimized_phase_mask_1.txt", phase_mask_1)
np.savetxt("results/optimized_phase_mask_2.txt", phase_mask_2)
print("All plots saved to results/ folder.")
+25
View File
@@ -0,0 +1,25 @@
% Model: cyljnderUnjtCell_solved.mph
% Versjon: COMSOL 6.1.0.282
% Date: Aug 7 2026, 11:04
% Table: Table 3 - Global Evaluatjon 3
% tPost (nm) R (nm) lambda0 (µm) S21 (1)
900 50 1.55 -0.845669386929+0.5044362611307165j
900 63.157894736842096 1.55 -0.8251549977113691+0.5387569255954204j
900 76.31578947368419 1.55 -0.7941914472021001+0.5854009666643545j
900 89.47368421052632 1.55 -0.7471355423796626+0.6469515347910716j
900 102.6315789473684 1.55 -0.6739731961216431+0.7260451361240156j
900 115.78947368421052 1.55 -0.5559252634519462+0.8236596878659627j
900 128.94736842105263 1.55 -0.35623973597781955+0.9314716653309569j
900 142.1052631578947 1.55 -0.005446778866258663+0.9995911107511558j
900 155.26315789473682 1.55 0.5624693373844357+0.8192539801083378j
900 168.42105263157896 1.55 0.9716453344468975-0.026879486169522106j
900 181.57894736842104 1.55 0.33229983484123915-0.9109913324745208j
900 194.73684210526312 1.55 -0.5138804677648452-0.8375112454139356j
900 207.89473684210526 1.55 -0.8979812315659875-0.41599951560137755j
900 221.05263157894737 1.55 -0.8666228661809928-0.35839703156981834j
900 234.21052631578942 1.55 -0.9706534677777011+0.1672691323977627j
900 247.36842105263153 1.55 -0.875683679549086+0.4301009457755804j
900 260.52631578947364 1.55 -0.7186563387658285+0.6394472785795947j
900 273.6842105263158 1.55 -0.6152456609406248+0.5634675095025711j
900 286.8421052631579 1.55 -0.3669553952916399+0.7919528013254294j
900 300 1.55 -0.07785011501821787+0.7863645334693915j
+31
View File
@@ -0,0 +1,31 @@
nohup: ignoring input
Rank 1 is processing diameter 0.274 (index 8)
Rank 2 is processing diameter 0.448 (index 16)
Rank 0 is processing diameter 0.100 (index 0)
Rank 0 is processing diameter 0.122 (index 1)
Rank 1 is processing diameter 0.296 (index 9)
Rank 0 is processing diameter 0.143 (index 2)
Rank 1 is processing diameter 0.317 (index 10)
Rank 2 is processing diameter 0.470 (index 17)
Rank 0 is processing diameter 0.165 (index 3)
Rank 1 is processing diameter 0.339 (index 11)
Rank 0 is processing diameter 0.187 (index 4)
Rank 1 is processing diameter 0.361 (index 12)
Rank 2 is processing diameter 0.491 (index 18)
Rank 0 is processing diameter 0.209 (index 5)
Rank 1 is processing diameter 0.383 (index 13)
Rank 2 is processing diameter 0.513 (index 19)
Rank 0 is processing diameter 0.230 (index 6)
Rank 1 is processing diameter 0.404 (index 14)
Rank 2 is processing diameter 0.535 (index 20)
Rank 0 is processing diameter 0.252 (index 7)
Rank 1 is processing diameter 0.426 (index 15)
Rank 2 is processing diameter 0.557 (index 21)
Rank 2 is processing diameter 0.578 (index 22)
Rank 2 is processing diameter 0.600 (index 23)
Elapsed run time = 61296.2745 s
Elapsed run time = 61296.2909 s
Elapsed run time = 61299.1873 s
+31
View File
@@ -0,0 +1,31 @@
nohup: ignoring input
Rank 2: Running simulation for diameter 0.4478260869565217
Rank 1: Running simulation for diameter 0.27391304347826084
Rank 0: Running simulation for diameter 0.1
Rank 0: Running simulation for diameter 0.12173913043478261
Rank 1: Running simulation for diameter 0.29565217391304344
Rank 0: Running simulation for diameter 0.14347826086956522
Rank 1: Running simulation for diameter 0.3173913043478261
Rank 2: Running simulation for diameter 0.4695652173913043
Rank 0: Running simulation for diameter 0.16521739130434782
Rank 1: Running simulation for diameter 0.33913043478260874
Rank 0: Running simulation for diameter 0.18695652173913044
Rank 1: Running simulation for diameter 0.36086956521739133
Rank 2: Running simulation for diameter 0.4913043478260869
Rank 0: Running simulation for diameter 0.20869565217391306
Rank 1: Running simulation for diameter 0.3826086956521739
Rank 2: Running simulation for diameter 0.5130434782608696
Rank 0: Running simulation for diameter 0.23043478260869565
Rank 1: Running simulation for diameter 0.4043478260869565
Rank 2: Running simulation for diameter 0.5347826086956522
Rank 0: Running simulation for diameter 0.25217391304347825
Rank 1: Running simulation for diameter 0.4260869565217391
Rank 2: Running simulation for diameter 0.5565217391304348
Elapsed run time = 5816.1358 s
Rank 2: Running simulation for diameter 0.5782608695652174
Rank 2: Running simulation for diameter 0.6
Elapsed run time = 10194.4932 s
Elapsed run time = 10198.0417 s
+169
View File
@@ -0,0 +1,169 @@
import meep as mp
mp.verbosity(0)
import numpy as np
import matplotlib.pyplot as plt
from mpi4py import MPI
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
size = comm.Get_size()
resolution = 80 # pixels/unit length
wvl_min = 1.5
wvl_max = 1.6
wvl_cen = 0.5*(wvl_min+wvl_max)
fmin = 1/wvl_max
fmax = 1/wvl_min
fcen = 1/wvl_cen
df = fmax-fmin
nfreq = 1
dpml = 1.0 #PML thickness
dsub = 1.5 + wvl_cen #substrate thickness
dpad = dsub #padding between cell and PML
k_point = mp.Vector3(0,0,0) #used for PBC
n_si = 3.45 #silicon refractive index
si = mp.Medium(index=n_si)
n_sio2 = 1.45
sio2 = mp.Medium(index=n_sio2)
n_air = 1.0 #air refractive index
air = mp.Medium(index=n_air)
P = 0.7 #unit cell pitch
h = 0.9 #pillar height
def unitCell_paramsweep(D):
sim_x_size = dpml + dsub + dpad + dpml
sim_z_size = P
sim_y_size = P
cell_size = mp.Vector3(sim_x_size, sim_y_size, sim_z_size)
source_position = mp.Vector3(-wvl_cen,0,0) #source position in the middle of the substrate
source = mp.Source(
mp.GaussianSource(fcen, fwidth=df),
component = mp.Ez,
center = source_position,
size = mp.Vector3(0, sim_y_size, sim_z_size),
)
pml_layers = [mp.PML(dpml,direction=mp.X)]
monitor_position = mp.Vector3(h + wvl_cen,0,0)
obs_point = monitor_position
stop_cond = mp.stop_when_fields_decayed(50, mp.Ez, obs_point, 1e-9)
#stop_cond = 10
sim = mp.Simulation(
resolution = resolution,
cell_size = cell_size,
boundary_layers = pml_layers,
k_point = k_point,
default_material = sio2,
sources = [source],
)
flux_mon = sim.add_flux(
[fcen],
mp.FluxRegion(center=monitor_position, size=mp.Vector3(0, sim_y_size, sim_z_size))
)
sim.run(until_after_sources=stop_cond)
input_flux = mp.get_fluxes(flux_mon)
sim.reset_meep()
pillar_geo = mp.Cylinder(
material = si,
radius = D/2,
height = h,
center = mp.Vector3(h/2,0,0),
axis=mp.Vector3(1,0,0),
)
substrate_geo = mp.Block(
material = sio2,
size = mp.Vector3(dsub+dpml, mp.inf, mp.inf),
center = mp.Vector3(-(dsub+dpml)/2,0,0),
)
geometries = [substrate_geo, pillar_geo]
sim = mp.Simulation(
resolution = resolution,
cell_size = cell_size,
boundary_layers = pml_layers,
geometry = geometries,
k_point = k_point,
sources = [source],
)
mode_mon = sim.add_flux(
[fcen],
mp.FluxRegion(center=monitor_position, size=mp.Vector3(0, sim_y_size, sim_z_size))
)
sim.run(until_after_sources=stop_cond)
freqs = mp.get_eigenmode_freqs(mode_mon)
res = sim.get_eigenmode_coefficients(
mode_mon, [1], eig_parity=mp.ODD_Z + mp.EVEN_Y
)
coeffs = res.alpha
mode_wvl = [1/freqs[nf] for nf in range(nfreq)]
mode_tran = [abs(coeffs[0,nf,0])**2 / input_flux[nf] for nf in range(nfreq)]
mode_phase = [np.angle(coeffs[0,nf,0]) for nf in range(nfreq)]
return mode_wvl, mode_tran, mode_phase
if rank == 0:
diameters = np.linspace(0.1, 0.6, 24)
else:
diameters = None
diameters = comm.bcast(diameters, root=0)
local_diameters = np.array_split(diameters, size)[rank]
local_indices = np.array_split(np.arange(len(diameters)), size)[rank]
local_mode_tran_arr = np.zeros(len(diameters))
local_mode_phase_arr = np.zeros(len(diameters))
for local_i, (global_i, D) in enumerate(zip(local_indices, local_diameters)):
print(f"Rank {rank} is processing diameter {D:.3f} (index {global_i})")
mode_wvl, mode_tran, mode_phase = unitCell_paramsweep(D)
local_mode_tran_arr[global_i] = mode_tran[0]
local_mode_phase_arr[global_i] = mode_phase[0]
comm.Barrier()
mode_tran_arr = comm.gather(local_mode_tran_arr, root=0)
mode_phase_arr = comm.gather(local_mode_phase_arr, root=0)
final_mode_tran_arr = np.zeros(len(diameters))
final_mode_phase_arr = np.zeros(len(diameters))
if rank == 0:
for i in range(size):
final_mode_tran_arr += mode_tran_arr[i]
final_mode_phase_arr += mode_phase_arr[i]
np.save("unit_cell_center_wvl_diameters.npy", diameters)
np.save("unit_cell_center_wvl_mode_wvl.npy", mode_wvl)
np.save("unit_cell_center_wvl_mode_tran.npy", final_mode_tran_arr)
np.save("unit_cell_center_wvl_mode_phase.npy", final_mode_phase_arr)
plt.figure()
plt.subplot(1,2,1)
plt.plot(diameters,final_mode_tran_arr)
plt.xlabel("Pillar Diameter (um)")
plt.ylabel("Transmission")
plt.subplot(1,2,2)
plt.plot(diameters,final_mode_phase_arr)
plt.xlabel("Pillar Diameter (um)")
plt.ylabel("Phase (rad)")
plt.subplots_adjust(wspace=0.5)
plt.savefig("unit_cell_fcen.pdf")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
+203
View File
@@ -0,0 +1,203 @@
import meep as mp
mp.verbosity(0)
import numpy as np
import matplotlib.pyplot as plt
from mpi4py import MPI
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
size = comm.Get_size()
resolution = 50 # pixels/unit length
wvl_min = 1.5
wvl_max = 1.6
wvl_cen = 0.5*(wvl_min+wvl_max)
fmin = 1/wvl_max
fmax = 1/wvl_min
fcen = 1/wvl_cen
df = fmax-fmin
nfreq = 20
dpml = 1.0 #PML thickness
dsub = 1.5 + wvl_cen #substrate thickness
dpad = dsub #padding between cell and PML
k_point = mp.Vector3(0,0,0) #used for PBC
n_si = 3.45 #silicon refractive index
si = mp.Medium(index=n_si)
n_sio2 = 1.45
sio2 = mp.Medium(index=n_sio2)
n_air = 1.0 #air refractive index
air = mp.Medium(index=n_air)
P = 0.7 #unit cell pitch
h = 0.9 #pillar height
def unitCell_paramsweep(D):
print("Rank {}: Running simulation for diameter {}".format(rank, D))
sim_x_size = dpml + dsub + dpad + dpml
sim_z_size = P
sim_y_size = P
cell_size = mp.Vector3(sim_x_size, sim_y_size, sim_z_size)
source_position = mp.Vector3(-wvl_cen,0,0) #source position in the middle of the substrate
source = mp.Source(
mp.GaussianSource(fcen, fwidth=df),
component = mp.Ez,
center = source_position,
size = mp.Vector3(0, sim_y_size, sim_z_size),
)
pml_layers = [mp.PML(dpml,direction=mp.X)]
monitor_position = mp.Vector3(h + wvl_cen,0,0)
obs_point = monitor_position
stop_cond = mp.stop_when_fields_decayed(50, mp.Ez, obs_point, 1e-9)
#stop_cond = 10
sim = mp.Simulation(
resolution = resolution,
cell_size = cell_size,
boundary_layers = pml_layers,
k_point = k_point,
default_material = sio2,
sources = [source],
)
flux_mon = sim.add_flux(
fcen,
df,
nfreq,
mp.FluxRegion(center=monitor_position, size=mp.Vector3(0, sim_y_size, sim_z_size))
)
sim.run(until_after_sources=stop_cond)
input_flux = mp.get_fluxes(flux_mon)
sim.reset_meep()
pillar_geo = mp.Cylinder(
material = si,
radius = D/2,
height = h,
center = mp.Vector3(h/2,0,0),
axis=mp.Vector3(1,0,0),
)
substrate_geo = mp.Block(
material = sio2,
size = mp.Vector3(dsub+dpml, mp.inf, mp.inf),
center = mp.Vector3(-(dsub+dpml)/2,0,0),
)
geometries = [substrate_geo, pillar_geo]
sim = mp.Simulation(
resolution = resolution,
cell_size = cell_size,
boundary_layers = pml_layers,
geometry = geometries,
k_point = k_point,
sources = [source],
)
mode_mon = sim.add_flux(
fcen, df, nfreq, mp.FluxRegion(center=monitor_position, size=mp.Vector3(0, sim_y_size, sim_z_size))
)
sim.run(until_after_sources=stop_cond)
freqs = mp.get_eigenmode_freqs(mode_mon)
res = sim.get_eigenmode_coefficients(
mode_mon, [1], eig_parity=mp.ODD_Z + mp.EVEN_Y
)
coeffs = res.alpha
mode_wvl = [1/freqs[nf] for nf in range(nfreq)]
mode_tran = [abs(coeffs[0,nf,0])**2 / input_flux[nf] for nf in range(nfreq)]
mode_phase = [np.angle(coeffs[0,nf,0]) for nf in range(nfreq)]
return mode_wvl, mode_tran, mode_phase
if rank == 0:
diameters = np.linspace(0.1, 0.6, 24)
else:
diameters = None
diameters = comm.bcast(diameters, root=0)
local_diameters = np.array_split(diameters, size)[rank]
local_indices = np.array_split(np.arange(len(diameters)), size)[rank]
local_mode_tran_arr = np.zeros((len(diameters), nfreq))
local_mode_phase_arr = np.zeros((len(diameters), nfreq))
for local_i, (global_i, D) in enumerate(zip(local_indices,local_diameters)):
mode_wvl, mode_tran, mode_phase = unitCell_paramsweep(D)
local_mode_tran_arr[global_i,:] = mode_tran
local_mode_phase_arr[global_i,:] = mode_phase
mode_tran_arr = comm.gather(local_mode_tran_arr, root=0)
mode_phase_arr = comm.gather(local_mode_phase_arr, root=0)
if rank == 0:
final_mode_tran_arr = np.zeros((len(diameters), nfreq))
final_mode_phase_arr = np.zeros((len(diameters), nfreq))
num_diameters_per_rank = len(diameters) // size
for i in range(0, size):
for j in range(num_diameters_per_rank):
global_index = i * num_diameters_per_rank + j
final_mode_tran_arr[global_index, :] = mode_tran_arr[i][global_index, :]
final_mode_phase_arr[global_index, :] = mode_phase_arr[i][global_index, :]
np.save("unit_cell_param_sweep_diameters.npy", diameters)
np.save("unit_cell_param_sweep_mode_wvl.npy", mode_wvl)
np.save("unit_cell_param_sweep_mode_tran.npy", final_mode_tran_arr)
np.save("unit_cell_param_sweep_mode_phase.npy", final_mode_phase_arr)
plt.figure()
plt.subplot(1,2,1)
plt.pcolormesh(
mode_wvl,
diameters,
final_mode_tran_arr,
cmap='hot_r',
shading='gouraud',
vmin=0,
vmax=final_mode_tran_arr.max(),
)
plt.axis([wvl_min, wvl_max, diameters[0], diameters[-1]])
plt.xlabel("Wavelength (um)")
plt.xticks([t for t in np.linspace(wvl_min, wvl_max, 3)])
plt.ylabel("Pillar Diameter (um)")
plt.title("Transmission")
cbar=plt.colorbar()
cbar.set_ticks([t for t in np.arange(0, 1.2, 0.2)])
plt.subplot(1,2,2)
plt.pcolormesh(
mode_wvl,
diameters,
final_mode_phase_arr,
cmap='RdBu',
shading='gouraud',
vmin=final_mode_phase_arr.min(),
vmax=final_mode_phase_arr.max(),
)
plt.axis([wvl_min, wvl_max, diameters[0], diameters[-1]])
plt.xlabel("Wavelength (um)")
plt.xticks([t for t in np.linspace(wvl_min, wvl_max, 3)])
plt.ylabel("Pillar Diameter (um)")
plt.title("phase")
cbar=plt.colorbar()
cbar.set_ticks([t for t in np.arange(-3, 4, 1)])
plt.subplots_adjust(wspace=0.5)
plt.savefig("unit_cell_param_sweep.pdf")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+41
View File
@@ -0,0 +1,41 @@
import numpy as np
import matplotlib.pyplot as plt
#import the parameter sweep arrays for the central wavelength
diameters = np.load("unit_cell_center_1550_h900nm_res50_wvl_diameters.npy")
mode_wvl = np.load("unit_cell_center_1550_h900nm_res50_wvl_mode_wvl.npy")
mode_tran_arr = np.load("unit_cell_center_1550_h900nm_res50_wvl_mode_tran.npy")
print(mode_tran_arr)
mode_phase_arr = np.load("unit_cell_center_1550_h900nm_res50_wvl_mode_phase.npy")
print(mode_wvl)
unwrap_mode_phase_arr = np.abs(np.unwrap(mode_phase_arr - mode_phase_arr[0], period=2*np.pi))
#import the reference value array
ref_results = np.loadtxt("results_S21_p700nm_h900nm.txt", skiprows=5,dtype=complex)
R_ref = np.real(ref_results[:,1])
S21_ref = ref_results[:,3]
S21_ref_mag = np.abs(S21_ref)
S21_ref_phase = np.abs(np.unwrap(np.angle(S21_ref) - np.angle(S21_ref[0]), period=2*np.pi))
plt.figure()
plt.subplot(1,2,1)
plt.plot(1e3*diameters/2,mode_tran_arr, '^r-.', label='FDTD')
plt.plot(R_ref,S21_ref_mag, '*k--',label='FEM')
plt.xlabel("Pillar Radius (um)")
plt.xticks([t for t in np.linspace(1e3*diameters[0]/2, 1e3*diameters[-1]/2, 5)])
plt.gca().set_xticklabels([f"{t:.0f}" for t in np.linspace(1e3*diameters[0]/2, 1e3*diameters[-1]/2, 5)])
plt.ylabel("Transmission")
plt.legend()
plt.subplot(1,2,2)
plt.plot(1e3*diameters/2,unwrap_mode_phase_arr, '^r-.', label='FDTD')
plt.plot(R_ref,S21_ref_phase, '*k--', label='FEM')
plt.xlabel("Pillar Radius (um)")
plt.xticks([t for t in np.linspace(1e3*diameters[0]/2, 1e3*diameters[-1]/2, 5)])
plt.gca().set_xticklabels([f"{t:.0f}" for t in np.linspace(1e3*diameters[0]/2, 1e3*diameters[-1]/2, 5)])
plt.ylabel("Phase (rad)")
plt.legend()
plt.subplots_adjust(wspace=0.5)
plt.savefig("unit_cell_param_sweep_lineplot.pdf")