Files
MPID/test_gres.py

399 lines
16 KiB
Python

import numpy as np
import matplotlib.pyplot as plt
import pyfftw
import os
import nlopt
from scipy.sparse.linalg import gmres, LinearOperator
results_folder = "fold_gmres"
if not os.path.exists(results_folder):
os.makedirs(results_folder)
# Physical parameters
lda0 = 1.55e-6 # Wavelength [m]
k0 = 2 * np.pi / lda0
n_sub = 1.444 # Substrate index
lambda_sub = lda0 / n_sub
k_sub = 2 * np.pi / lambda_sub
substrate_thickness = 500e-6
# Grid definition
Nx = 1025
Ny = Nx
dof = Nx * Ny
unit_cell_pitch = 700e-9
size_x = unit_cell_pitch * Nx
size_y = unit_cell_pitch * Ny
xs = (np.arange(Nx) - Nx // 2) * unit_cell_pitch
ys = (np.arange(Ny) - Ny // 2) * unit_cell_pitch
X, Y = np.meshgrid(xs, ys)
# Spatial frequencies (FFT order)
kx = np.fft.fftfreq(Nx, d=unit_cell_pitch) * 2 * np.pi
ky = np.fft.fftfreq(Ny, d=unit_cell_pitch) * 2 * np.pi
Kx, Ky = np.meshgrid(kx, ky)
K_parallel_sq = Kx**2 + Ky**2
# Propagation Kernel (FFT order)
kz_sq = k_sub**2 - K_parallel_sq
propagating_mask = kz_sq >= 0
# Evanescent wave filtering
kz = np.where(propagating_mask, np.sqrt(np.maximum(0, kz_sq)), 0.0)
P_nat = np.where(propagating_mask, np.exp(1j * kz * substrate_thickness), 0.0)
P_adj = np.conj(P_nat.T)
# Source definition
pixel_shift = 400
source_radius = 21e-6
output_radius = 21e-6
source_mask = np.where(X**2 + Y**2 <= source_radius**2, 1.0, 0.0)
output_mask = np.where(X**2 + Y**2 <= output_radius**2, 1.0, 0.0)
source_mask = np.roll(source_mask, shift=(0, -pixel_shift), axis=(0, 1))
output_mask_1 = np.roll(output_mask, shift=(0, pixel_shift), axis=(0, 1))
output_mask_2 = np.roll(output_mask, shift=(int(60e-6/unit_cell_pitch), pixel_shift), axis=(0, 1))
output_mask_3 = np.roll(output_mask, shift=(int(-60e-6/unit_cell_pitch), pixel_shift), axis=(0, 1))
#output_mask = output_mask_1 + output_mask_2 + output_mask_3
output_mask = output_mask_1 #temporary
I_output = output_mask.flatten()
#import a fiber mode profile and position it over the source mask
source_fiber_mode1 = "fiber_modes/FMF/FMF_LP01_field_distribution_lda_1550_N_1025.npy"
source_fiber_mode2 = "fiber_modes/FMF/FMF_LP11_field_distribution_lda_1550_N_1025.npy"
source_fiber_mode3 = "fiber_modes/FMF/FMF_LP21_field_distribution_lda_1550_N_1025.npy"
target_fiber_mode1 = "fiber_modes/SMF/SMF_LP01_field_distribution_lda_1550_N_1025.npy"
target_fiber_mode2 = "fiber_modes/SMF/SMF_LP01_field_distribution_lda_1550_N_1025.npy"
target_fiber_mode3 = "fiber_modes/SMF/SMF_LP01_field_distribution_lda_1550_N_1025.npy"
#import the file and shift it by an
import_source_field_1 = np.load(source_fiber_mode1)
import_source_field_2 = np.load(source_fiber_mode2)
import_source_field_3 = np.load(source_fiber_mode3)
import_target_field_1 = np.load(target_fiber_mode1)
import_target_field_2 = np.load(target_fiber_mode2)
import_target_field_3 = np.load(target_fiber_mode3)
#pad the imported source fields
import_source_field_1 = np.pad(import_source_field_1, (((Nx - import_source_field_1.shape[0])//2,(Nx - import_source_field_1.shape[0])//2 ), ( (Ny - import_source_field_1.shape[1])//2, (Ny - import_source_field_1.shape[1])//2 )), mode='constant')
import_source_field_2 = np.pad(import_source_field_2, (((Nx - import_source_field_2.shape[0])//2,(Nx - import_source_field_2.shape[0])//2 ), ( (Ny - import_source_field_2.shape[1])//2, (Ny - import_source_field_2.shape[1])//2 )), mode='constant')
import_source_field_3 = np.pad(import_source_field_3, (((Nx - import_source_field_3.shape[0])//2,(Nx - import_source_field_3.shape[0])//2 ), ( (Ny - import_source_field_3.shape[1])//2, (Ny - import_source_field_3.shape[1])//2 )), mode='constant')
import_target_field_1 = np.pad(import_target_field_1, (((Nx - import_target_field_1.shape[0])//2,(Nx - import_target_field_1.shape[0])//2 ), ( (Ny - import_target_field_1.shape[1])//2, (Ny - import_target_field_1.shape[1])//2 )), mode='constant')
import_target_field_2 = np.pad(import_target_field_2, (((Nx - import_target_field_2.shape[0])//2,(Nx - import_target_field_2.shape[0])//2 ), ( (Ny - import_target_field_2.shape[1])//2, (Ny - import_target_field_2.shape[1])//2 )), mode='constant')
import_target_field_3 = np.pad(import_target_field_3, (((Nx - import_target_field_3.shape[0])//2,(Nx - import_target_field_3.shape[0])//2 ), ( (Ny - import_target_field_3.shape[1])//2, (Ny - import_target_field_3.shape[1])//2 )), mode='constant')
#shift the source fields to align with the source mask
source_field_1 = np.roll(import_source_field_1, shift=(0, -pixel_shift), axis=(0, 1)) / np.sum(np.abs(import_source_field_1) ** 2)
source_field_2 = np.roll(import_source_field_2, shift=(0, -pixel_shift), axis=(0, 1)) / np.sum(np.abs(import_source_field_2) ** 2)
source_field_3 = np.roll(import_source_field_3, shift=(0, -pixel_shift), axis=(0, 1)) / np.sum(np.abs(import_source_field_3) ** 2)
# Angle of incidence
input_angle = 8.0 / 180 * np.pi # radians
deflection_phase = np.exp(1j * k_sub * X * np.sin(input_angle))
tilted_field_1 = source_field_1 * deflection_phase
tilted_field_2 = source_field_2 * deflection_phase
tilted_field_3 = source_field_3 * deflection_phase
#target fields
target_field_1 = np.roll(import_target_field_1, shift=(0, -pixel_shift), axis=(0, 1)) / np.sum(np.abs(import_target_field_1) ** 2)
target_field_2 = np.roll(import_target_field_2, shift=(0, -pixel_shift), axis=(0, 1)) / np.sum(np.abs(import_target_field_2) ** 2)
target_field_3 = np.roll(import_target_field_3, shift=(0, -pixel_shift), axis=(0, 1)) / np.sum(np.abs(import_target_field_3) ** 2)
#phase mask boundary region
phase_mask_region = np.ones((Nx, Ny)) - output_mask - source_mask
boundary_X_width = 150 # number of pixels to exclude from the edges
boundary_Y_width = 150
phase_mask_region[:boundary_Y_width, :] = 0
phase_mask_region[-boundary_Y_width:, :] = 0
phase_mask_region[:, :boundary_X_width] = 0
phase_mask_region[:, -boundary_X_width:] = 0
# w_norm boundaries for nlopt
w_norm_upper_boundary = np.where(phase_mask_region,1,0)
w_norm_lower_boundary = np.zeros_like(w_norm_upper_boundary)
gmres_data_folder = 'gmres_data'
def initialize_phase_mask():
'''Utility function used to import phase masks from a previous
stacked simulation.'''
phase_mask_1 = np.load(f"{gmres_data_folder}/phase_mask_1.npy")
phase_mask_2 = np.load(f"{gmres_data_folder}/phase_mask_2.npy")
phase_mask_3 = np.load(f"{gmres_data_folder}/phase_mask_3.npy")
center_mask_1_x = 2 * substrate_thickness * np.atan(input_angle)
center_mask_1_y = 0
center_mask_2_x = 4 * substrate_thickness * np.atan(input_angle)
center_mask_2_y = 0
center_mask_3_x = 6 * substrate_thickness * np.atan(input_angle)
center_mask_3_y = 0
#pad the phase masks so that they have the same size as the spatial grid. By padding I mean adding
# zero-padding around the phase masks
phase_mask_1_padded = np.pad(phase_mask_1, ((Ny//2 - phase_mask_1.shape[0]//2, Ny//2 - phase_mask_1.shape[0]//2),
(Nx//2 - phase_mask_1.shape[1]//2, Nx//2 - phase_mask_1.shape[1]//2)),
mode='constant', constant_values=0)
phase_mask_2_padded = np.pad(phase_mask_2, ((Ny//2 - phase_mask_2.shape[0]//2, Ny//2 - phase_mask_2.shape[0]//2),
(Nx//2 - phase_mask_2.shape[1]//2, Nx//2 - phase_mask_2.shape[1]//2)),
mode='constant', constant_values=0)
phase_mask_3_padded = np.pad(phase_mask_3, ((Ny//2 - phase_mask_3.shape[0]//2, Ny//2 - phase_mask_3.shape[0]//2),
(Nx//2 - phase_mask_3.shape[1]//2, Nx//2 - phase_mask_3.shape[1]//2)),
mode='constant', constant_values=0)
overall_mask = np.roll(phase_mask_1_padded, shift=(center_mask_1_y//unit_cell_pitch, center_mask_1_x//unit_cell_pitch - pixel_shift), axis=(0, 1)) + \
np.roll(phase_mask_2_padded, shift=(center_mask_2_y//unit_cell_pitch, center_mask_2_x//unit_cell_pitch - pixel_shift), axis=(0, 1)) + \
np.roll(phase_mask_3_padded, shift=(center_mask_3_y//unit_cell_pitch, center_mask_3_x//unit_cell_pitch - pixel_shift), axis=(0, 1))
overall_mask = (overall_mask + 2*np.pi) % (2*np.pi)
plt.figure(figsize=(6, 5))
im=plt.pcolormesh(
X * 1e6, Y * 1e6,
overall_mask + source_mask + output_mask,
cmap = 'hsv',
shading="auto",
rasterized=True
)
plt.colorbar(im, label="Mask amplitude")
plt.scatter((center_mask_1_x - pixel_shift*unit_cell_pitch) * 1e6, center_mask_1_y * 1e6, color='white', marker='*', s=70)
plt.scatter((center_mask_2_x - pixel_shift*unit_cell_pitch) * 1e6, center_mask_2_y * 1e6, color='white', marker='*', s=70)
plt.scatter((center_mask_3_x - pixel_shift*unit_cell_pitch) * 1e6, center_mask_3_y * 1e6, color='white', marker='*', s=70)
plt.title("Overall Phase Mask")
plt.xlabel("X (µm)")
plt.ylabel("Y (µm)")
plt.gca().set_aspect("equal")
plt.savefig(f"{results_folder}/overall_phase_mask.pdf")
return (overall_mask*phase_mask_region).flatten()
#phase mask setup
w_norm = initialize_phase_mask() / (2 * np.pi)
phase_mask = np.zeros_like(w_norm)
u_k = np.zeros(dof, dtype=np.complex128) #initial guess for the intermediate field
Phi_2D = np.zeros((Ny, Nx), dtype=np.complex128)
#plot the boundary mask
plt.figure(figsize=(6, 5))
plt.pcolormesh(
X * 1e6, Y * 1e6,
phase_mask_region,
shading="auto", rasterized=True
)
plt.title("Boundary Mask")
plt.xlabel("X (µm)")
plt.ylabel("Y (µm)")
plt.gca().set_aspect("equal")
plt.colorbar(label="Mask amplitude")
plt.savefig(f"{results_folder}/boundary_mask.pdf")
#plot both input and output masks
plt.figure(figsize=(6, 5))
plt.pcolormesh(
X * 1e6, Y * 1e6,
source_mask,
shading="auto", rasterized=True
)
plt.title("Input Mask")
plt.xlabel("X (µm)")
plt.ylabel("Y (µm)")
plt.gca().set_aspect("equal")
plt.colorbar(label="Mask amplitude")
plt.savefig(f"{results_folder}/input_mask.pdf")
plt.figure(figsize=(6, 5))
plt.pcolormesh(
X * 1e6, Y * 1e6,
output_mask,
shading="auto", rasterized=True
)
plt.title("Output Mask")
plt.xlabel("X (µm)")
plt.ylabel("Y (µm)")
plt.gca().set_aspect("equal")
plt.colorbar(label="Mask amplitude")
plt.savefig(f"{results_folder}/output_mask.pdf")
#plot the source field
plt.figure(figsize=(6, 5))
plt.pcolormesh(
X * 1e6, Y * 1e6,
np.abs(source_field_1) / np.max(np.abs(source_field_1)),
shading="auto", rasterized=True
)
plt.title("Source Field Amplitude")
plt.xlabel("X (µm)")
plt.ylabel("Y (µm)")
plt.gca().set_aspect("equal")
plt.colorbar(label="Normalized amplitude")
plt.savefig(f"{results_folder}/source_field_amplitude.pdf")
def forward_propagate(field):
"""
Forward propagate a field using the angular spectrum method.
Args:
field : ndarray input field in the spatial domain (flattened).
Returns:
prop_field : ndarray Propagated field in the spatial domain (flattened).
"""
#make sure the input field is 2D
field_2D = np.reshape(field, (Nx, Ny)) # reshape to 2D array
field_k = np.fft.fft2(field_2D, norm='ortho')
field_k_prop = field_k * P_nat
prop_field = np.fft.ifft2(field_k_prop, norm='ortho').flatten()
return prop_field
def adjoint_propagate(field):
"""
Adjoint propagate a field using the angular spectrum method.
Args:
field : ndarray input field in the spatial domain (flattened).
Returns:
adj_field_prop : ndarray Propagated field in the spatial domain.
"""
field_2D = np.reshape(field, (Nx, Ny)) # reshape to 2D array
field_k = np.fft.fft2(field_2D, norm='ortho')
field_k_prop = field_k * P_adj
adj_prop_field = np.fft.ifft2(field_k_prop, norm='ortho').flatten()
return adj_prop_field
def phase_given_w(w_norm):
return 2 * np.pi * w_norm
def apply_m(u_k):
'''Applies the M matrix to a given uk vector, M=(Phi_1 U Phi_0 U)u_k,
where U=F^-1PF is the ASM propagation matrix
Args:
u_k : ndarray Input field vector in the spatial domain.
Returns:
res4.flatten(): the result after applying the M matrix, flattened to a 1D array.
'''
phi_0 = (1 - output_mask).flatten() #2D array
phi_1 = ((1 - source_mask) * Phi_2D).flatten() #2D array
res1 = forward_propagate(u_k)
res2 = res1 * phi_0
res3 = forward_propagate(res2)
res4 = res3 * phi_1
return res4.flatten()
def apply_m_adjoint(u_k):
'''Applies the adjoint of the M matrix to a given uk vector, M=(Phi_1 U Phi_0 U)^(dagger)u_k,
where U=F^-1PF is the ASM propagation matrix
Args:
u_k : ndarray Input field vector in the spatial domain.
Returns:
M_u_k.flatten(): the result after applying the adjoint of the M matrix to vector u_k, flattened to a 1D array.
'''
#phi_0 is the phase mask on the output plane
phi_0_adj = (1 - output_mask).flatten() #2D array
#phi_1 is the phase mask on the input plane.
# It is the top layer that stores the phase mask.
phi_1_adj = np.conj(((1 - source_mask) * Phi_2D).flatten()) #2D array
res0 = u_k * phi_1_adj
res1 = adjoint_propagate(res0)
res2 = res1 * phi_0_adj
res3 = adjoint_propagate(res2)
M_adj_u_k = res3.flatten()
return M_adj_u_k
def auk(u_k):
"""
Applies the A matrix to a given uk vector.
Args:
u_k : ndarray Input field vector in the spatial domain.
Returns:
res : ndarray Result after applying the A matrix.
"""
res = u_k - apply_m(u_k)
return res
def auk_adjoint(u_k):
"""
Applies the adjoint of the A matrix to a given uk vector.
Args:
u_k : ndarray Input field vector in the spatial domain.
Returns:
res : ndarray Result after applying the adjoint of the A matrix.
"""
res = u_k - apply_m_adjoint(u_k)
return res
def gmres_solver():
A_uk = LinearOperator((dof,dof), matvec=auk, dtype=np.complex128)
res_u_k, info = gmres(A_uk, tilted_field_1.flatten(), u_k, restart=50, maxiter=30)
if info != 0:
print(f"WARNING: GMRES did not converge, info: {info}")
return res_u_k
def adjoint_gmres_solver(s_adj):
adj_A_uk = LinearOperator((dof,dof), matvec=auk_adjoint, dtype=np.complex128)
res_u_k, info = gmres(adj_A_uk, s_adj, restart=50, maxiter=30)
if info != 0:
print(f"WARNING: adjoint GMRES did not converge, info: {info}")
return res_u_k
def cost_fun(x,grad):
"""
Cost function for the optimization algorithm.
Args:
x : ndarray Current guess for the optimization variables.
grad : ndarray Gradient of the cost function with respect to x.
Returns:
overlap: value of the cost function (float)
"""
w_norm[:] = x # update the current guess for the optimization variables, in place
phase_mask[:] = phase_given_w(w_norm)
Phi_2D[:,:] = np.exp(1j * phase_mask.reshape((Ny, Nx))) # construct the complex phase mask
#initiate the GMRES computation to find uk
print("Starting GMRES solver...")
u_k[:] = gmres_solver() # update the intermediate field using GMRES solver
print("Starting forward propagation...")
propagated_u_k = forward_propagate(u_k)
output_field = I_output * propagated_u_k # compute the output field based on the updated intermediate field
fig,ax = plt.subplots()
ax.pcolormesh(X*1e6, Y*1e6, np.abs(propagated_u_k.reshape((Ny, Nx))), shading='auto', rasterized=True)
ax.set_xlabel("X [um]")
ax.set_ylabel("Y [um]")
plt.savefig(f"{results_folder}/output_field.pdf")
tilde_S = np.sum(output_field * np.conj(target_field_1.flatten()))
overlap = (np.abs(tilde_S) ** 2) # compute the overlap with the target field
print("Overlap = ", overlap)
print("Starting adjoint GMRES solver...")
s_adj = tilde_S * adjoint_propagate(I_output * target_field_1.flatten())
v = adjoint_gmres_solver(s_adj)
w = apply_m(u_k)
#update gradients in place
if grad.size > 0:
grad[:] = -2 * np.imag(np.conj(v) * w) * 2 * np.pi
print("Finished iteration")
return overlap
if __name__ == "__main__":
opt_max_eval = 140 #max number of optimization evaluations
# #setup the optimization algorithm
solver = nlopt.opt(nlopt.LD_CCSAQ, dof)
solver.set_lower_bounds(w_norm_lower_boundary.flatten())
solver.set_upper_bounds(w_norm_upper_boundary.flatten())
print("shape of boundaries",w_norm_upper_boundary.flatten().shape)
solver.set_max_objective(cost_fun)
solver.set_maxeval(opt_max_eval)
print("w_norm shape", w_norm.shape)
solver.optimize(w_norm)