mirror of
https://github.com/edoverg/modeconverter.git
synced 2026-10-14 23:06:39 +00:00
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:
@@ -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.")
|
||||
@@ -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.")
|
||||
@@ -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.")
|
||||
@@ -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()
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
@@ -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.
@@ -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")
|
||||
Reference in New Issue
Block a user