implemented new filtering system based on a Filter Class. Previously, filtering was made expensive by extreme padding, now this has been fixed by choosing a more appropriate padding size. The gradient chain based on unitcells has been implemented but the results have yet to be tested using FDTD, which will be the next step. Added some utilities to visualize the effect of filter radius on given input masks and added all data concerning unit cell simulations. Still to be done is to fix materials in the MEEP simulations, so as to account for true material behavior instead of relying on constant refractive indices.

This commit is contained in:
Edoardo Vergerio
2026-10-14 16:40:05 +02:00
parent 7a71b43b72
commit 625cd64f53
34 changed files with 1891 additions and 196 deletions
+38 -24
View File
@@ -1,8 +1,32 @@
import autograd.numpy as npa
import numpy as np
from scipy.fft import next_fast_len
from numpy.typing import ArrayLike as ArrayLikeType
import matplotlib.pyplot as plt
def _centered(arr: npa.ndarray, newshape: ArrayLikeType) -> npa.ndarray:
class Filter:
def __init__(self, radius, space_grid):
self.radius = radius
self.space_grid = space_grid
self.p = int(np.ceil(radius / self.space_grid.get_resolution()))
self.Fx = next_fast_len(self.space_grid.get_Nx() + self.p)
self.Fy = next_fast_len(self.space_grid.get_Ny() + self.p)
self.H = self._construct_kernel(radius, space_grid)
def apply_filter(self, input_parameters):
input_parameters_2d = input_parameters.reshape(self.space_grid.get_Nx(), self.space_grid.get_Ny())
input_parameters_padded = self._edge_pad(input_parameters_2d, (((self.Fx-1)//2, (self.Fx)//2), ((self.Fy-1)//2, (self.Fy)//2)))
#print("input_parameters_padded shape:",input_parameters_padded.shape)
input_parameters_fft = npa.fft.fft2(input_parameters_padded)
Y = input_parameters_fft * self.H
y = self._centered(
npa.real(npa.fft.ifft2(Y)),
(self.space_grid.get_Nx(), self.space_grid.get_Ny())
)
return y.flatten()
def _centered(self, arr: npa.ndarray, newshape: ArrayLikeType) -> npa.ndarray:
"""Formats the output of an FFT to center the zero-frequency component.
A helper function borrowed from SciPy:
@@ -24,7 +48,7 @@ def _centered(arr: npa.ndarray, newshape: ArrayLikeType) -> npa.ndarray:
return arr[tuple(myslice)]
def _edge_pad(arr: npa.ndarray, pad: npa.ndarray) -> npa.ndarray:
def _edge_pad(self, arr: npa.ndarray, pad: npa.ndarray) -> npa.ndarray:
"""Border-pads the edges of an array.
Used to preprocess the design weights prior to convolution with the filter.
@@ -70,7 +94,7 @@ def _edge_pad(arr: npa.ndarray, pad: npa.ndarray) -> npa.ndarray:
else:
raise ValueError("At least one of the padding numbers is invalid.")
def _quarter_to_full_kernel(arr: npa.ndarray, pad_to: npa.ndarray) -> npa.ndarray:
def _quarter_to_full_kernel(self, arr: npa.ndarray, pad_to: npa.ndarray) -> npa.ndarray:
"""Constructs the full kernel from its nonnegative quadrant.
Args:
@@ -104,17 +128,17 @@ def _quarter_to_full_kernel(arr: npa.ndarray, pad_to: npa.ndarray) -> npa.ndarra
axis=1,
)
def conic_filter(phase_mask,radius,space_grid):
'''Applies a conic filter to the input phase mask. The filter is defined as a
conic region of a specified radius, where the values are averaged.
def _construct_kernel(self,radius,space_grid):
'''Applies a conic filter to the input parameters. The filter is defined as a
conic region of a specified radius, where the values undergo a weighted average.
Args:
phase_mask: the input phase mask to be filtered (2D)
input_parameters: the input parameters to be filtered (2D)
radius: the radius of the conic filter in micrometers
space_grid: an object containing the size and resolution of the phase mask
space_grid: a SpaceGrid object containing the size and resolution of the parameters
Returns:
The filtered phase mask (2D)
The filtered parameters (2D)
'''
Nx = space_grid.get_Nx()
Ny = space_grid.get_Ny()
@@ -127,22 +151,12 @@ def conic_filter(phase_mask,radius,space_grid):
h = npa.where(Xv**2 + Yv**2 < radius**2, (1 - npa.sqrt(abs(Xv**2 + Yv**2)) / radius),0)
h = _quarter_to_full_kernel(h, 3 * npa.array([Nx, Ny]))
#impulse response is padded to [Fx,Fy]
h = self._quarter_to_full_kernel(h, npa.array([self.Fx + Nx - 1, self.Fy + Ny - 1]))
print("Fx, Fy:", self.Fx, self.Fy)
h = h / npa.sum(h)
#make a plot of the filter kernel
H = npa.fft.fft2(h)
phase_mask_2d = phase_mask.reshape(Nx,Ny)
phase_mask_padded = _edge_pad(phase_mask_2d, ((Nx, Nx), (Ny, Ny)))
phase_mask_fft = npa.fft.fft2(phase_mask_padded)
Y = phase_mask_fft * H
y = _centered(
npa.real(npa.fft.ifft2(Y)),
(Nx, Ny)
)
return y.flatten()
return H
+116 -45
View File
@@ -7,9 +7,9 @@ import pyfftw
import nlopt
from autograd import tensor_jacobian_product, grad as gd
import autograd.numpy as npa
from filters import conic_filter
from filters import Filter
def make_2Dplot_of(given_field:np.ndarray, grid_X:np.ndarray, grid_Y:np.ndarray, plot_zoom_x:float,plot_zoom_y:float,choose_quantity="amplitude",save_name="plot") -> None:
def make_2Dplot_of(given_field:np.ndarray, grid_X:np.ndarray, grid_Y:np.ndarray, plot_zoom_x:float, plot_zoom_y:float, choose_quantity="amplitude", save_name="plot") -> 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:
@@ -25,7 +25,6 @@ def make_2Dplot_of(given_field:np.ndarray, grid_X:np.ndarray, grid_Y:np.ndarray,
fig, ax = plt.subplots(figsize=(6, 6))
if choose_quantity == "amplitude":
ax.pcolormesh(grid_X*1e6,grid_Y*1e6,np.abs(given_field_2d),rasterized=True)
#add a colorbar for amplitude
plt.colorbar(ax.pcolormesh(grid_X*1e6,grid_Y*1e6,np.abs(given_field_2d),rasterized=True), ax=ax)
elif choose_quantity == "phase":
ax.pcolormesh(grid_X*1e6,grid_Y*1e6,(np.angle(given_field_2d)+2*np.pi)%(2*np.pi), cmap='hsv', vmin=0, vmax=2*np.pi, rasterized=True)
@@ -596,8 +595,10 @@ class Simulation:
if conic_filter_radius is not None:
self.filter_radius = conic_filter_radius
self.filter_obj = Filter(conic_filter_radius, self.space_grid)
else:
self.filter_radius = None
self.filter_obj = None
def get_sources(self):
return self.sources
@@ -632,7 +633,8 @@ class Simulation:
def _update_wavelength(self, new_wavelength):
'''
Updates the operating wavelength and related vectors (lda_vector and k_vectors) based on the new wavelength.
The new_wavelength should be provided in meters.
The new_wavelength should be provided in meters. If use_unit_cells is True, updating the wavelength
will also update the current active unit cell.
Args:
new_wavelength (float): The new operating wavelength in meters.
'''
@@ -771,9 +773,10 @@ class Simulation:
def _normParam_filter(self, norm_param: np.ndarray) -> np.ndarray:
'''Applies a conic filter on normalized parameters. Normalized parameters are the ones the optimization algorithm
uses and are always in the range [0,1].'''
return conic_filter(norm_param,
self.filter_radius,
self.space_grid,)
# return conic_filter(norm_param,
# self.filter_radius,
# self.space_grid,)
return self.filter_obj.apply_filter(norm_param)
def _phase_filter(self, phase_mask: np.ndarray) -> np.ndarray:
'''Applies a conic filter on the phase mask. The filtering action is equivalent to a local weighted averaging of the phase.
@@ -794,17 +797,18 @@ class Simulation:
phase_mask_cos = npa.cos(phase_mask)
phase_mask_sin = npa.sin(phase_mask)
phase_mask_filtered_cos = conic_filter(
phase_mask_cos,
self.filter_radius,
self.space_grid,
)
phase_mask_filtered_sin = conic_filter(
phase_mask_sin,
self.filter_radius,
self.space_grid,
)
# phase_mask_filtered_cos = conic_filter(
# phase_mask_cos,
# self.filter_radius,
# self.space_grid,
# )
phase_mask_filtered_cos = self.filter_obj.apply_filter(phase_mask_cos)
# phase_mask_filtered_sin = conic_filter(
# phase_mask_sin,
# self.filter_radius,
# self.space_grid,
# )
phase_mask_filtered_sin = self.filter_obj.apply_filter(phase_mask_sin)
return npa.arctan2(phase_mask_filtered_sin, phase_mask_filtered_cos)
def _forward_propagate(self, source_object: Source, returns_field: bool = False):
@@ -1184,7 +1188,8 @@ class Simulation:
#after the phase mask is set, we test the same mask across all targets
C_all_wavelengths = []
tested_wavelengths = []
grad_partial = np.zeros_like(grad)
#per-mask gradient w.r.t. the filter output, accumulated over targets (the filter Jacobian is target-independent)
grads_wrt_filtered = None
for target in self.targets:
@@ -1197,22 +1202,27 @@ class Simulation:
full_grads = [g * self.current_unit_cell.get_complete_gradient_from_norm_param(fw)
for g, fw in zip(full_grads, filtered_w_masks)]
#the gradient depends on wavelength, because phase of the unit cell is wavelength-dependent.
#Each mask accumulates only its own gradient.
if grads_wrt_filtered is None:
grads_wrt_filtered = [np.array(g, dtype=float) for g in full_grads]
else:
for k, g in enumerate(full_grads):
grads_wrt_filtered[k] += g
#a single backward pass per mask through the filter, which acts on w (unit cells) or on the phase (no unit cells)
if self.use_filtering:
#backpropagate through the filter, which acts on w (unit cells) or on the phase (no unit cells)
if self.use_unit_cells:
filter_fun, filter_inputs = self._normParam_filter, w_masks
else:
filter_fun, filter_inputs = self._phase_filter, phase_masks
jac_grads = [tensor_jacobian_product(filter_fun)(inp, g) for inp, g in zip(filter_inputs, full_grads)]
jac_grads = [tensor_jacobian_product(filter_fun)(inp, g) for inp, g in zip(filter_inputs, grads_wrt_filtered)]
else:
jac_grads = list(full_grads)
jac_grads = list(grads_wrt_filtered)
if self.space_grid.xy_symmetry:
jac_grads = [self._reduce_symmetrized_gradient(j) for j in jac_grads]
jac_grad = np.concatenate(jac_grads, axis=0) # = (dC / dPhi) * (dw_f / dw), recall Phi = 2pi * w_f
#the gradient depends on wavelength, because phase of the unit cell is wavelength-dependent
grad_partial[:] += jac_grad
grad_partial = np.concatenate(jac_grads, axis=0) # = sum over targets of (dC / dPhi) * (dw_f / dw), recall Phi = 2pi * w_f
if grad.size > 0:
@@ -1454,64 +1464,125 @@ class Simulation:
self.set_space_grid(new_space_grid)
def inject_phase_masks(self, input_phase_masks: list):
def inject_phase_masks(self, input_phase_masks: list, apply_filtering: bool = True):
"""
Inject the specified phase masks into the simulation.
Inject the specified phase masks into the simulation, optionally applying filtering based on the `apply_filtering` flag.
Args:
input_phase_masks (list): A list of phase masks, flattened, to be injected into the simulation.
apply_filtering (bool): Apply filtering to the injected phase masks. Defaults to True.
Returns:
None
"""
#check if the length of the list of input_phase_masks we are trying to inject
#has the same number of phase masks in this simulation object.
if len(input_phase_masks) != self.num_phase_masks:
raise ValueError(f"Expected {self.num_phase_masks} phase masks, but got {len(input_phase_masks)}.")
for i, phase_mask in enumerate(input_phase_masks):
#set the phase mask attribute with the input phase masks
self.phase_mask[i*self.space_grid.get_S():(i+1)*self.space_grid.get_S()] = phase_mask
self.w_norm[i*self.space_grid.get_S():(i+1)*self.space_grid.get_S()] = self._w_given_phase(phase_mask)
if self.use_filtering:
#self.w_norm[i*self.space_grid.get_S():(i+1)*self.space_grid.get_S()] = self._w_given_phase(phase_mask)
if apply_filtering: #filtering is applied regardless of the global optimization setting
if self.use_unit_cells:
selected_filter = lambda x: self._normParam_filter(x)
else:
selected_filter = lambda x: self._phase_filter(x)
if self.num_phase_masks == 2:
filtered_phase_mask_1 = self._phase_filter(self.phase_mask[:self.space_grid.get_S()])
filtered_phase_mask_2 = self._phase_filter(self.phase_mask[self.space_grid.get_S():])
filtered_phase_mask_1 = selected_filter(self.phase_mask[:self.space_grid.get_S()])
filtered_phase_mask_2 = selected_filter(self.phase_mask[self.space_grid.get_S():])
self.filtered_phase_mask = np.concatenate((filtered_phase_mask_1, filtered_phase_mask_2))
elif self.num_phase_masks == 3:
filtered_phase_mask_1 = self._phase_filter(self.phase_mask[:self.space_grid.get_S()])
filtered_phase_mask_2 = self._phase_filter(self.phase_mask[self.space_grid.get_S():2*self.space_grid.get_S()])
filtered_phase_mask_3 = self._phase_filter(self.phase_mask[2*self.space_grid.get_S():])
filtered_phase_mask_1 = selected_filter(self.phase_mask[:self.space_grid.get_S()])
filtered_phase_mask_2 = selected_filter(self.phase_mask[self.space_grid.get_S():2*self.space_grid.get_S()])
filtered_phase_mask_3 = selected_filter(self.phase_mask[2*self.space_grid.get_S():])
self.filtered_phase_mask = np.concatenate((filtered_phase_mask_1, filtered_phase_mask_2, filtered_phase_mask_3))
else:
self.filtered_phase_mask = self.phase_mask
def source_sweep(self, distances: np.ndarray = None, input_phase_masks: list = None):
def _update_phase_masks_with_diameters(self, input_diameters_masks: list, apply_filtering: bool = False):
"""
This method runs the propagation algorithm for the specified sources and targets in the simulation object.
For every propagation step, it enforces the same phase masks. It can optionally update propagation distances and phase masks.
If not specified, it is assumed the simulation object has already been optimized and that the required quantities are already set.
It is useful for studying the wavelength-dependent performance of the system.
Constructs the phase masks based on the provided diameter masks and the currently active unit cell.
Args:
distances (np.ndarray, optional): Array containing the propagation distances. If None, the current distances are used.
input_diameters_masks (list): A list of diameter masks, flattened, to be used for updating the phase masks.
Returns:
None
"""
if len(input_diameters_masks) != self.num_phase_masks:
raise ValueError(f"Expected {self.num_phase_masks} masks, but got {len(input_diameters_masks)}.")
#check if unit current unit cell is set and not None
if self.current_unit_cell is None:
raise ValueError("Cannot generate phase masks from diameters if no unit cell is provided.")
mask1 = input_diameters_masks[0]
mask2 = input_diameters_masks[1] if self.num_phase_masks > 1 else None
mask3 = input_diameters_masks[2] if self.num_phase_masks > 2 else None
phase_mask_1 = self.current_unit_cell.get_phase_at_param(mask1)
phase_mask_2 = self.current_unit_cell.get_phase_at_param(mask2)
phase_mask_3 = self.current_unit_cell.get_phase_at_param(mask3) if mask3 is not None else None
phase_mask_list = [phase_mask_1, phase_mask_2, phase_mask_3][:self.num_phase_masks]
self.inject_phase_masks(phase_mask_list, apply_filtering=apply_filtering)
def source_sweep(self, distances: np.ndarray = None, input_phase_masks: list = None, input_diameter_masks: list = None, apply_filtering: bool = True, save_output: bool = False):
"""
Runs the propagation algorithm for the specified sources and targets in the simulation object.
There are two operating modes of this method: if phase masks are provided via the `input_phase_masks` argument,
If 'input_diameter_masks' is provided, phase masks are
updated based on the source wavelength and unit cell data. If none of the two are provided, the last phase masks
One cannot provide both `input_phase_masks` and `input_diameter_masks` simultaneously.
Example: if the simulation contains two targets T1 and T2, with associated sources S1, S2, at two distinct wavelengths L1, L2, passing input_diameters_masks
will update the phase masks for each source by selecting the appropriate unit cell based on the source wavelength and the provided diameter mask.
Unit cell data must be already be present in the simulation object.
It is typically used for studying the wavelength-dependent performance of the system.
Args:
distances (np.ndarray, optional): Array containing the propagation distances. If None, the current distances in the simulation object are used.
input_phase_masks (list, optional): A list of phase masks to be injected into the simulation. If None, the current phase masks are used.
input_diameter_masks (list, optional): A list of diameter masks to be used for updating the phase masks based on the source wavelength and unit cell data.
apply_filtering (bool, optional): Controls filtering with higher priority than Simulation settings. Defaults to True.
save_output (bool, optional): Save the output results to a file. Default is False.
Returns:
dict: A dictionary mapping source wavelengths to the overlap between the output field and the associated target field.
"""
#setup
if input_diameter_masks is not None and input_phase_masks is not None:
raise ValueError("Cannot choose: both input_phase_masks and input_diameter_masks were provided.")
results_dict = {}
if distances is not None:
self.update_propagation_distances(distances)
if input_phase_masks is not None:
self.inject_phase_masks(input_phase_masks)
self.inject_phase_masks(input_phase_masks, apply_filtering=apply_filtering)
if input_diameter_masks is not None:
use_unit_cells = True
else:
use_unit_cells = False
#simulate
for target in self.targets:
source = target.get_associated_source()
self._update_wavelength(source.get_wavelength())
wavelength = source.get_wavelength()
self._update_wavelength(wavelength)
#retrieve the appropriate phase mask from the simulation's active unit cell
if use_unit_cells:
self._update_phase_masks_with_diameters(input_diameter_masks, apply_filtering=apply_filtering)
output_field = self._forward_propagate(source, returns_field = True)
#compute the overlap between the output field and associated target field
target_field = target.get_field(self.get_space_grid(), flatten = True)
overlap = np.abs(np.sum(np.conj(target_field) * output_field))**2
results_dict[source.get_wavelength()] = overlap
if save_output:
save_filename = f"{self.results_folder}/output_field_{wavelength*1e6}"
save_filename = save_filename.replace('.', '_') + ".npy"
np.save(save_filename, output_field)
return results_dict
+1 -1
View File
@@ -25,7 +25,7 @@ d = np.array([d0, d1, d2])
ms_max_available_diameter = 122 #[um]
output_fiber_core_diameter = 19 #[um]
opt_max_eval = 1000
opt_max_eval = 600
space_grid = pid.SpaceGrid(
side_pixels=N,
+16 -2
View File
@@ -30,7 +30,7 @@ mat_refr_index = np.array([n0, n1, n2])
ms_max_available_diameter = 122 #[um]
output_fiber_core_diameter = 19 #[um]
opt_max_eval = 140
opt_max_eval = 550
def setup_optical_system()->pid.Simulation:
'''
@@ -47,7 +47,7 @@ def setup_optical_system()->pid.Simulation:
'''
central_wavelength_um = 1.55
wavelengths_to_test_um = [1.530, 1.550, 1.570] #[um]
wavelengths_to_test_um = [1.53, 1.55, 1.57] #[um]
source_files = ["fiber_modes/SMF/SMF_LP01_field_distribution_lda_1530_N_1025.npy",
"fiber_modes/SMF/SMF_LP01_field_distribution_lda_1550_N_1025.npy",
@@ -66,6 +66,19 @@ def setup_optical_system()->pid.Simulation:
wavelength=wvl,
field_norm=True) for i, (file, wvl) in enumerate(zip(target_files, wavelengths_to_test_um))]
#import unit cell data
unit_cell_diameters_wvl_1530 = np.load("unit_cell/unit_cell_center_1530_h900nm_res50_wvl_diameters.npy")
unit_cell_diameters_wvl_1550 = np.load("unit_cell/unit_cell_center_1550_h900nm_res50_wvl_diameters.npy")
unit_cell_diameters_wvl_1570 = np.load("unit_cell/unit_cell_center_1570_h900nm_res50_wvl_diameters.npy")
unit_cell_phases_wvl_1530 = np.load("unit_cell/unit_cell_center_1530_h900nm_res50_wvl_mode_phase.npy")
unit_cell_phases_wvl_1550 = np.load("unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_phase.npy")
unit_cell_phases_wvl_1570 = np.load("unit_cell/unit_cell_center_1570_h900nm_res50_wvl_mode_phase.npy")
#construct the UnitCell objects
unit_cell_1530 = pid.UnitCell(wavelength=1.53, tune_param=unit_cell_diameters_wvl_1530, sim_phase=unit_cell_phases_wvl_1530)
unit_cell_1550 = pid.UnitCell(wavelength=1.55, tune_param=unit_cell_diameters_wvl_1550, sim_phase=unit_cell_phases_wvl_1550)
unit_cell_1570 = pid.UnitCell(wavelength=1.57, tune_param=unit_cell_diameters_wvl_1570, sim_phase=unit_cell_phases_wvl_1570)
unit_cells_list = [unit_cell_1530, unit_cell_1550, unit_cell_1570]
initial_space_grid = pid.SpaceGrid(
side_pixels=N,
unit_cell_pitch=unit_cell_pitch,
@@ -89,6 +102,7 @@ def setup_optical_system()->pid.Simulation:
phase_mask_init_size=output_fiber_core_diameter/3,
opt_max_eval=opt_max_eval,
conic_filter_radius=1.5,
unit_cells=unit_cells_list,
results_while_solving = False,
save_results = False,
)
+137 -17
View File
@@ -17,6 +17,18 @@ ms_max_available_diameter = 122 #[um]
output_fiber_core_diameter = 19 #[um]
opt_max_eval = 140
central_wavelength_um = 1.55 #[um]
wavelengths_to_test_um = [1.530, 1.550, 1.570] #[um]
source_files = ["fiber_modes/SMF/SMF_LP01_field_distribution_lda_1530_N_1025.npy",
"fiber_modes/SMF/SMF_LP01_field_distribution_lda_1550_N_1025.npy",
"fiber_modes/SMF/SMF_LP01_field_distribution_lda_1570_N_1025.npy"]
target_files = ["fiber_modes/FMF/FMF_LP21_field_distribution_lda_1530_N_1025.npy",
"fiber_modes/FMF/FMF_LP21_field_distribution_lda_1550_N_1025.npy",
"fiber_modes/FMF/FMF_LP21_field_distribution_lda_1570_N_1025.npy"]
def setup_optical_system()->pid.Simulation:
'''
Builds the mode converter optical system using
@@ -30,27 +42,28 @@ def setup_optical_system()->pid.Simulation:
Returns:
float: The best objective value obtained from the optimization.
'''
central_wavelength_um = 1.55
wavelengths_to_test_um = [1.530, 1.550, 1.570] #[um]
source_files = ["fiber_modes/SMF/SMF_LP01_field_distribution_lda_1530_N_1025.npy",
"fiber_modes/SMF/SMF_LP01_field_distribution_lda_1550_N_1025.npy",
"fiber_modes/SMF/SMF_LP01_field_distribution_lda_1570_N_1025.npy"]
sources = [pid.Source(input_field=np.load(file),
wavelength=wvl,
field_norm=True) for file, wvl in zip(source_files, wavelengths_to_test_um)]
target_files = ["fiber_modes/FMF/FMF_LP21_field_distribution_lda_1530_N_1025.npy",
"fiber_modes/FMF/FMF_LP21_field_distribution_lda_1550_N_1025.npy",
"fiber_modes/FMF/FMF_LP21_field_distribution_lda_1570_N_1025.npy"]
targets = [pid.Target(input_field=np.load(file),
associated_source=sources[i],
wavelength=wvl,
field_norm=True) for i, (file, wvl) in enumerate(zip(target_files, wavelengths_to_test_um))]
#import unit cell data
unit_cell_diameters_wvl_1530 = np.load("unit_cell/unit_cell_center_1530_h900nm_res50_wvl_diameters.npy")
unit_cell_diameters_wvl_1550 = np.load("unit_cell/unit_cell_center_1550_h900nm_res50_wvl_diameters.npy")
unit_cell_diameters_wvl_1570 = np.load("unit_cell/unit_cell_center_1570_h900nm_res50_wvl_diameters.npy")
unit_cell_phases_wvl_1530 = np.load("unit_cell/unit_cell_center_1530_h900nm_res50_wvl_mode_phase.npy")
unit_cell_phases_wvl_1550 = np.load("unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_phase.npy")
unit_cell_phases_wvl_1570 = np.load("unit_cell/unit_cell_center_1570_h900nm_res50_wvl_mode_phase.npy")
#construct the UnitCell objects
unit_cell_1530 = pid.UnitCell(wavelength=1.530, tune_param=unit_cell_diameters_wvl_1530, sim_phase=unit_cell_phases_wvl_1530)
unit_cell_1550 = pid.UnitCell(wavelength=1.550, tune_param=unit_cell_diameters_wvl_1550, sim_phase=unit_cell_phases_wvl_1550)
unit_cell_1570 = pid.UnitCell(wavelength=1.570, tune_param=unit_cell_diameters_wvl_1570, sim_phase=unit_cell_phases_wvl_1570)
unit_cells_list = [unit_cell_1530, unit_cell_1550, unit_cell_1570]
initial_space_grid = pid.SpaceGrid(
side_pixels=N,
unit_cell_pitch=unit_cell_pitch,
@@ -73,7 +86,8 @@ def setup_optical_system()->pid.Simulation:
ms_max_avail_diameter=ms_max_available_diameter,
phase_mask_init_size=output_fiber_core_diameter/3,
opt_max_eval=opt_max_eval,
#conic_filter_radius=1.0,
conic_filter_radius=1.0,
unit_cells=unit_cells_list,
results_while_solving = False,
save_results = False,
)
@@ -84,17 +98,19 @@ def setup_optical_system()->pid.Simulation:
if __name__ == "__main__":
simulation = setup_optical_system()
results_folder_name = 'results_nf'
results_folder_name = 'results'
phase_mask_1 = np.load(f"{results_folder_name}/filtered_phase_mask_1.npy").flatten()
phase_mask_2 = np.load(f"{results_folder_name}/filtered_phase_mask_2.npy").flatten()
#phase_mask_1 = np.load(f"{results_folder_name}/filtered_phase_mask_1.npy").flatten()
#phase_mask_2 = np.load(f"{results_folder_name}/filtered_phase_mask_2.npy").flatten()
mask1 = np.load(f"{results_folder_name}/mask_1.npy").flatten()
mask2 = np.load(f"{results_folder_name}/mask_2.npy").flatten()
d0 = 195 #propagation distance: source - MS1 [um] FILTERED mf (using LP01 of SMF)
d1 = 129 #propagation distance: MS1 - MS2 [um] FILTERED mf (using LP01 of SMF)
d2 = d0 #propagation distance: MS2 - target [um]
d = np.array([d0, d1, d2])
results = simulation.source_sweep(distances = d, input_phase_masks=[phase_mask_1, phase_mask_2])
results = simulation.source_sweep(distances = d, input_diameter_masks=[mask1, mask2], save_output=True, apply_filtering=False)
#make a plot of the results, Use the keys as x axis and values as y axis
fig, ax = plt.subplots(figsize=(6, 6))
ax.plot(np.array(list(results.keys()))*1e9, np.array(list(results.values())), marker='o', linewidth=2)
@@ -106,3 +122,107 @@ if __name__ == "__main__":
ax.set_aspect('auto')
plt.tight_layout()
plt.savefig(f"{results_folder_name}/source_sweep_results.pdf")
#for each of the test wavelength, take the fields from the results folder and make plots of them
for i, wvl in enumerate(wavelengths_to_test_um):
save_filename = f"{results_folder_name}/output_field_{wvl}"
save_filename = save_filename.replace('.', '_')
output_field = np.load(save_filename+ ".npy").reshape(simulation.space_grid.get_Nx(), simulation.space_grid.get_Ny())
fig, ax = plt.subplots(figsize=(6, 6))
ax.pcolormesh(simulation.space_grid.get_X()*1e6, simulation.space_grid.get_Y()*1e6, np.abs(output_field) / np.max(np.abs(output_field)), cmap='viridis', rasterized = True)
colorbar = fig.colorbar(ax.collections[0], ax=ax, label='Magnitude [a.u.]')
ax.set_aspect('equal')
ax.tick_params(axis='x', labelsize=20)
ax.tick_params(axis='y', labelsize=20)
#set the tick fontsize to 20
ax.tick_params(axis='both', which='major', labelsize=20)
ax.set_xlim(-output_fiber_core_diameter/2-10, output_fiber_core_diameter/2+10)
ax.set_ylim(-output_fiber_core_diameter/2-10, output_fiber_core_diameter/2+10)
#overlay a red circle indicating the output fiber core
circle = plt.Circle((0, 0), output_fiber_core_diameter/2, color='red', fill=False, linewidth=2)
ax.add_artist(circle)
ax.set_xlabel('X [um]', fontsize=20)
ax.set_ylabel('Y [um]', fontsize=20)
plt.title(r'Output Field at $\lambda=$' + str(wvl) + r' $\mu$m')
plt.tight_layout()
plt.savefig(save_filename + ".pdf")
plt.close()
fig, ax = plt.subplots(figsize=(6, 6))
ax.pcolormesh(simulation.space_grid.get_X()*1e6, simulation.space_grid.get_Y()*1e6, (np.angle(output_field)+2*np.pi)%(2*np.pi), cmap='hsv', vmin=0, vmax=2*np.pi, rasterized = True)
colorbar = fig.colorbar(ax.collections[0], ax=ax, label='Phase [rad]')
ax.set_aspect('equal')
ax.tick_params(axis='x', labelsize=20)
ax.tick_params(axis='y', labelsize=20)
#set the tick fontsize to 20
ax.tick_params(axis='both', which='major', labelsize=20)
ax.set_xlim(-output_fiber_core_diameter/2-10, output_fiber_core_diameter/2+10)
ax.set_ylim(-output_fiber_core_diameter/2-10, output_fiber_core_diameter/2+10)
#overlay a red circle indicating the output fiber core
circle = plt.Circle((0, 0), output_fiber_core_diameter/2, color='red', fill=False, linewidth=2)
ax.add_artist(circle)
ax.set_xlabel('X [um]', fontsize=20)
ax.set_ylabel('Y [um]', fontsize=20)
plt.title(r'Output Field Phase at $\lambda=$' + str(wvl) + r' $\mu$m')
plt.tight_layout()
plt.savefig(save_filename + "_phase.pdf")
plt.close()
target_field_filename = target_files[i]
target_field = np.load(target_field_filename)
fig, axs = plt.subplots(2, 2, figsize=(12, 12))
axs = axs.flatten()
# First subplot: output field amplitude
im = axs[0].pcolormesh(simulation.space_grid.get_X()*1e6, simulation.space_grid.get_Y()*1e6, np.abs(output_field)/np.max(np.abs(output_field)), cmap='viridis', shading='auto', rasterized=True)
fig.colorbar(im, ax=axs[0], label='Magnitude [a.u.]')
axs[0].set_aspect('equal')
axs[0].set_xlim(-output_fiber_core_diameter/2-10, output_fiber_core_diameter/2+10)
axs[0].set_ylim(-output_fiber_core_diameter/2-10, output_fiber_core_diameter/2+10)
circle = plt.Circle((0, 0), output_fiber_core_diameter/2, color='red', fill=False, linewidth=2)
axs[0].add_artist(circle)
axs[0].set_xlabel('X [um]', fontsize=20)
axs[0].set_ylabel('Y [um]', fontsize=20)
axs[0].tick_params(axis='x', labelsize=20)
axs[0].tick_params(axis='y', labelsize=20)
axs[0].tick_params(axis='both', which='major', labelsize=20)
axs[0].set_title('Output Field Amplitude')
axs[0].set_aspect('equal')
# Second subplot: y cross section slice
y_slice = output_field[:, output_field.shape[1]//2]
y_target_slice = target_field[:, target_field.shape[1]//2]
axs[1].plot(simulation.space_grid.get_Y()[:, 0]*1e6, np.abs(y_slice)/np.max(np.abs(y_slice)), label='Output', c='blue', linewidth=2)
axs[1].plot(simulation.space_grid.get_Y()[:, 0]*1e6, np.abs(y_target_slice)/np.max(np.abs(y_target_slice)), label='Target', c='red', linewidth=2)
axs[1].set_xlabel('Y [um]', fontsize=20)
axs[1].set_ylabel('Magnitude [a.u.]', fontsize=20)
axs[1].set_title('Y Cross Section')
axs[1].set_ylim(0, 1)
axs[1].set_xlim(-output_fiber_core_diameter/2-10, output_fiber_core_diameter/2+10)
axs[1].legend(fontsize=20)
axs[1].set_aspect('auto')
axs[1].tick_params(axis='x', labelsize=20)
axs[1].tick_params(axis='y', labelsize=20)
axs[1].tick_params(axis='both', which='major', labelsize=20)
# Third subplot: x cross section slice
x_slice = output_field[output_field.shape[0]//2, :]
x_target_slice = target_field[target_field.shape[0]//2, :]
axs[2].plot(simulation.space_grid.get_X()[0, :]*1e6, np.abs(x_slice)/np.max(np.abs(x_slice)), label='Output', c='blue', linewidth=2)
axs[2].plot(simulation.space_grid.get_X()[0, :]*1e6, np.abs(x_target_slice)/np.max(np.abs(x_target_slice)), label='Target', c='red', linewidth=2)
axs[2].set_xlabel('X [um]', fontsize=20)
axs[2].set_ylim(0, 1)
axs[2].set_xlim(-output_fiber_core_diameter/2-10, output_fiber_core_diameter/2+10)
axs[2].set_ylabel('Magnitude [a.u.]', fontsize=20)
axs[2].set_title('X Cross Section')
axs[2].legend(fontsize=20)
axs[2].set_aspect('auto')
axs[2].tick_params(axis='x', labelsize=20)
axs[2].tick_params(axis='y', labelsize=20)
axs[2].tick_params(axis='both', which='major', labelsize=20)
#remove completely the fourth subplot axis since we only have three now
fig.delaxes(axs[3])
plt.tight_layout()
plt.savefig(save_filename + "_comparison.pdf")
plt.close()
+55
View File
@@ -0,0 +1,55 @@
#use this file to import some test phase masks and observe
#the effect of changing the filtering radius
import numpy as np
import matplotlib.pyplot as plt
from autograd import tensor_jacobian_product
class TempGrid:
def __init__(self, Nx, Ny, res): self.Nx, self.Ny, self.res = Nx, Ny, res
def get_Nx(self): return self.Nx
def get_Ny(self): return self.Ny
def get_resolution(self): return self.res
grid = TempGrid(Nx=1025, Ny=1025, res=0.7)
radius_to_test = [2.0, 5.0, 11.0]
input_mask = np.load('results/mask_2.npy')
#make a plot of the input mask
# fig, ax = plt.subplots()
# ax.imshow(input_mask, cmap='inferno')
# #zoom in the center region
# center_x, center_y = grid.get_Nx() // 2, grid.get_Ny() // 2
# zoom_size = 100 # adjust as needed
# ax.set_xlim(center_x - zoom_size, center_x + zoom_size)
# ax.set_ylim(center_y - zoom_size, center_y + zoom_size)
# plt.savefig('input_mask.pdf')
#apply the filter
from filters import Filter
center_x, center_y = grid.get_Nx() // 2, grid.get_Ny() // 2
zoom_size = 80
colormap = 'gray_r'
fig, ax = plt.subplots(1,4, figsize=(12, 3))
ax[0].imshow(input_mask, cmap=colormap)
ax[0].set_title('Input', fontsize=20)
ax[0].set_xlim(center_x - zoom_size, center_x + zoom_size)
ax[0].set_ylim(center_y - zoom_size, center_y + zoom_size)
ax[0].set_xticks([])
ax[0].set_yticks([])
for i, radius in enumerate(radius_to_test, start=1):
filt = Filter(radius, grid)
output_mask = filt.apply_filter(input_mask)
ax[i].imshow(output_mask.reshape(grid.get_Nx(), grid.get_Ny()), cmap=colormap)
ax[i].set_title(f'r={radius} \u03BCm', fontsize=20)
ax[i].set_xlim(center_x - zoom_size, center_x + zoom_size)
ax[i].set_ylim(center_y - zoom_size, center_y + zoom_size)
ax[i].set_xticks([])
ax[i].set_yticks([])
plt.tight_layout()
plt.savefig('filtered_masks.pdf')
+29
View File
@@ -0,0 +1,29 @@
nohup: ignoring input
Rank 0 is processing diameter 0.100 (index 0)
Rank 1 is processing diameter 0.361 (index 12)
Rank 0 is processing diameter 0.122 (index 1)
Rank 1 is processing diameter 0.383 (index 13)
Rank 0 is processing diameter 0.143 (index 2)
Rank 1 is processing diameter 0.404 (index 14)
Rank 0 is processing diameter 0.165 (index 3)
Rank 1 is processing diameter 0.426 (index 15)
Rank 0 is processing diameter 0.187 (index 4)
Rank 1 is processing diameter 0.448 (index 16)
Rank 0 is processing diameter 0.209 (index 5)
Rank 1 is processing diameter 0.470 (index 17)
Rank 0 is processing diameter 0.230 (index 6)
Rank 0 is processing diameter 0.252 (index 7)
Rank 1 is processing diameter 0.491 (index 18)
Rank 0 is processing diameter 0.274 (index 8)
Rank 1 is processing diameter 0.513 (index 19)
Rank 0 is processing diameter 0.296 (index 9)
Rank 1 is processing diameter 0.535 (index 20)
Rank 0 is processing diameter 0.317 (index 10)
Rank 1 is processing diameter 0.557 (index 21)
Rank 0 is processing diameter 0.339 (index 11)
Rank 1 is processing diameter 0.578 (index 22)
Rank 1 is processing diameter 0.600 (index 23)
Elapsed run time = 17749.7805 s
Elapsed run time = 17750.1165 s
+25
View File
@@ -0,0 +1,25 @@
% Model: cyljnderUnjtCell_solved.mph
% Versjon: COMSOL 6.1.0.282
% Date: Aug 7 2026, 11:04
% Table: Table 3 - Global Evaluatjon 3
% tPost (nm) R (nm) lambda0 (µm) S21 (1)
900 50 1.55 -0.845669386929+0.5044362611307165j
900 63.157894736842096 1.55 -0.8251549977113691+0.5387569255954204j
900 76.31578947368419 1.55 -0.7941914472021001+0.5854009666643545j
900 89.47368421052632 1.55 -0.7471355423796626+0.6469515347910716j
900 102.6315789473684 1.55 -0.6739731961216431+0.7260451361240156j
900 115.78947368421052 1.55 -0.5559252634519462+0.8236596878659627j
900 128.94736842105263 1.55 -0.35623973597781955+0.9314716653309569j
900 142.1052631578947 1.55 -0.005446778866258663+0.9995911107511558j
900 155.26315789473682 1.55 0.5624693373844357+0.8192539801083378j
900 168.42105263157896 1.55 0.9716453344468975-0.026879486169522106j
900 181.57894736842104 1.55 0.33229983484123915-0.9109913324745208j
900 194.73684210526312 1.55 -0.5138804677648452-0.8375112454139356j
900 207.89473684210526 1.55 -0.8979812315659875-0.41599951560137755j
900 221.05263157894737 1.55 -0.8666228661809928-0.35839703156981834j
900 234.21052631578942 1.55 -0.9706534677777011+0.1672691323977627j
900 247.36842105263153 1.55 -0.875683679549086+0.4301009457755804j
900 260.52631578947364 1.55 -0.7186563387658285+0.6394472785795947j
900 273.6842105263158 1.55 -0.6152456609406248+0.5634675095025711j
900 286.8421052631579 1.55 -0.3669553952916399+0.7919528013254294j
900 300 1.55 -0.07785011501821787+0.7863645334693915j
+51
View File
@@ -0,0 +1,51 @@
'''
Utility script to test the gradients functionality of the unit cell class of mpid
Given the simulated transmittance phase values and associated tune parameter,
the unit cell class should return the gradient of the simulated phase with respect to the tuning parameter.
the class also provides a method to compute the gradient at a specific tuning parameter value, through interpolation.
'''
import numpy as np
from matplotlib import pyplot as plt
from mpid import UnitCell
wavelength_um_1_53 = 1.53
wavelength_um_1_55 = 1.55
wavelength_um_1_57 = 1.57
unit_cell_diameters = np.load("unit_cell/unit_cell_center_1550_h900nm_res50_wvl_diameters.npy")
unit_cell_phases_wvl_1530 = np.load("unit_cell/unit_cell_center_1530_h900nm_res50_wvl_mode_phase.npy")
unit_cell_phases_wvl_1550 = np.load("unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_phase.npy")
unit_cell_phases_wvl_1570 = np.load("unit_cell/unit_cell_center_1570_h900nm_res50_wvl_mode_phase.npy")
unit_cell_1530 = UnitCell(wavelength=wavelength_um_1_53, tune_param=unit_cell_diameters, sim_phase=unit_cell_phases_wvl_1530)
unit_cell_1550 = UnitCell(wavelength=wavelength_um_1_55, tune_param=unit_cell_diameters, sim_phase=unit_cell_phases_wvl_1550)
unit_cell_1570 = UnitCell(wavelength=wavelength_um_1_57, tune_param=unit_cell_diameters, sim_phase=unit_cell_phases_wvl_1570)
fig, ax = plt.subplots(figsize=(6, 6))
plt.rcParams['mathtext.default'] = 'regular'
ax.plot(unit_cell_1530.get_tune_param(), unit_cell_1530.get_gradient(), color='r', label=r"$\lambda=1530\:[nm]$", linewidth=3)
ax.plot(unit_cell_1550.get_tune_param(), unit_cell_1550.get_gradient(), color='g', label=r"$\lambda=1550\:[nm]$", linewidth=3)
ax.plot(unit_cell_1570.get_tune_param(), unit_cell_1570.get_gradient(), color='b', label=r"$\lambda=1570\:[nm]$", linewidth=3)
ax.set_xlabel("diameter [um]", fontsize=20)
ax.set_ylabel(r"$\partial \phi/\partial d$", fontsize=20, rotation='vertical')
ax.yaxis.set_label_coords(-0.11, 0.5)
ax.set_aspect('auto')
ax.set_xticks(np.arange(min(unit_cell_diameters), max(unit_cell_diameters)+0.1, 0.1))
ax.tick_params(axis='x', labelsize=20)
ax.tick_params(axis='y', labelsize=20)
plt.tight_layout()
plt.savefig("unit_cell_gradient.pdf")
fig, ax = plt.subplots(figsize=(6, 6))
ax.plot(unit_cell_1530.get_tune_param(), unit_cell_1530.get_sim_phase(), color='r', label=r"$\lambda=1530\:[nm]$", linewidth=3)
ax.plot(unit_cell_1550.get_tune_param(), unit_cell_1550.get_sim_phase(), color='g', label=r"$\lambda=1550\:[nm]$", linewidth=3)
ax.plot(unit_cell_1570.get_tune_param(), unit_cell_1570.get_sim_phase(), color='b', label=r"$\lambda=1570\:[nm]$", linewidth=3)
ax.set_xlabel("diameter [um]", fontsize=20)
ax.set_ylabel(r"$\phi\:$[rad]", fontsize=20)
ax.tick_params(axis='x', labelsize=20)
ax.tick_params(axis='y', labelsize=20)
ax.legend(fontsize=20, loc='best')
ax.set_aspect('auto')
ax.set_xticks(np.arange(min(unit_cell_diameters), max(unit_cell_diameters)+0.1, 0.1))
plt.tight_layout()
plt.savefig("unit_cell_phase.pdf")
+169
View File
@@ -0,0 +1,169 @@
import meep as mp
mp.verbosity(0)
import numpy as np
import matplotlib.pyplot as plt
from mpi4py import MPI
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
size = comm.Get_size()
resolution = 50 # pixels/unit length
wvl_min = 1.53
wvl_max = 1.57
wvl_cen = 1.57
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.444
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.
File diff suppressed because one or more lines are too long
Binary file not shown.
+203
View File
@@ -0,0 +1,203 @@
import meep as mp
mp.verbosity(0)
import numpy as np
import matplotlib.pyplot as plt
from mpi4py import MPI
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
size = comm.Get_size()
resolution = 50 # pixels/unit length
wvl_min = 1.5
wvl_max = 1.6
wvl_cen = 0.5*(wvl_min+wvl_max)
fmin = 1/wvl_max
fmax = 1/wvl_min
fcen = 1/wvl_cen
df = fmax-fmin
nfreq = 20
dpml = 1.0 #PML thickness
dsub = 1.5 + wvl_cen #substrate thickness
dpad = dsub #padding between cell and PML
k_point = mp.Vector3(0,0,0) #used for PBC
n_si = 3.45 #silicon refractive index
si = mp.Medium(index=n_si)
n_sio2 = 1.45
sio2 = mp.Medium(index=n_sio2)
n_air = 1.0 #air refractive index
air = mp.Medium(index=n_air)
P = 0.7 #unit cell pitch
h = 0.9 #pillar height
def unitCell_paramsweep(D):
print("Rank {}: Running simulation for diameter {}".format(rank, D))
sim_x_size = dpml + dsub + dpad + dpml
sim_z_size = P
sim_y_size = P
cell_size = mp.Vector3(sim_x_size, sim_y_size, sim_z_size)
source_position = mp.Vector3(-wvl_cen,0,0) #source position in the middle of the substrate
source = mp.Source(
mp.GaussianSource(fcen, fwidth=df),
component = mp.Ez,
center = source_position,
size = mp.Vector3(0, sim_y_size, sim_z_size),
)
pml_layers = [mp.PML(dpml,direction=mp.X)]
monitor_position = mp.Vector3(h + wvl_cen,0,0)
obs_point = monitor_position
stop_cond = mp.stop_when_fields_decayed(50, mp.Ez, obs_point, 1e-9)
#stop_cond = 10
sim = mp.Simulation(
resolution = resolution,
cell_size = cell_size,
boundary_layers = pml_layers,
k_point = k_point,
default_material = sio2,
sources = [source],
)
flux_mon = sim.add_flux(
fcen,
df,
nfreq,
mp.FluxRegion(center=monitor_position, size=mp.Vector3(0, sim_y_size, sim_z_size))
)
sim.run(until_after_sources=stop_cond)
input_flux = mp.get_fluxes(flux_mon)
sim.reset_meep()
pillar_geo = mp.Cylinder(
material = si,
radius = D/2,
height = h,
center = mp.Vector3(h/2,0,0),
axis=mp.Vector3(1,0,0),
)
substrate_geo = mp.Block(
material = sio2,
size = mp.Vector3(dsub+dpml, mp.inf, mp.inf),
center = mp.Vector3(-(dsub+dpml)/2,0,0),
)
geometries = [substrate_geo, pillar_geo]
sim = mp.Simulation(
resolution = resolution,
cell_size = cell_size,
boundary_layers = pml_layers,
geometry = geometries,
k_point = k_point,
sources = [source],
)
mode_mon = sim.add_flux(
fcen, df, nfreq, mp.FluxRegion(center=monitor_position, size=mp.Vector3(0, sim_y_size, sim_z_size))
)
sim.run(until_after_sources=stop_cond)
freqs = mp.get_eigenmode_freqs(mode_mon)
res = sim.get_eigenmode_coefficients(
mode_mon, [1], eig_parity=mp.ODD_Z + mp.EVEN_Y
)
coeffs = res.alpha
mode_wvl = [1/freqs[nf] for nf in range(nfreq)]
mode_tran = [abs(coeffs[0,nf,0])**2 / input_flux[nf] for nf in range(nfreq)]
mode_phase = [np.angle(coeffs[0,nf,0]) for nf in range(nfreq)]
return mode_wvl, mode_tran, mode_phase
if rank == 0:
diameters = np.linspace(0.1, 0.6, 24)
else:
diameters = None
diameters = comm.bcast(diameters, root=0)
local_diameters = np.array_split(diameters, size)[rank]
local_indices = np.array_split(np.arange(len(diameters)), size)[rank]
local_mode_tran_arr = np.zeros((len(diameters), nfreq))
local_mode_phase_arr = np.zeros((len(diameters), nfreq))
for local_i, (global_i, D) in enumerate(zip(local_indices,local_diameters)):
mode_wvl, mode_tran, mode_phase = unitCell_paramsweep(D)
local_mode_tran_arr[global_i,:] = mode_tran
local_mode_phase_arr[global_i,:] = mode_phase
mode_tran_arr = comm.gather(local_mode_tran_arr, root=0)
mode_phase_arr = comm.gather(local_mode_phase_arr, root=0)
if rank == 0:
final_mode_tran_arr = np.zeros((len(diameters), nfreq))
final_mode_phase_arr = np.zeros((len(diameters), nfreq))
num_diameters_per_rank = len(diameters) // size
for i in range(0, size):
for j in range(num_diameters_per_rank):
global_index = i * num_diameters_per_rank + j
final_mode_tran_arr[global_index, :] = mode_tran_arr[i][global_index, :]
final_mode_phase_arr[global_index, :] = mode_phase_arr[i][global_index, :]
np.save("unit_cell_param_sweep_diameters.npy", diameters)
np.save("unit_cell_param_sweep_mode_wvl.npy", mode_wvl)
np.save("unit_cell_param_sweep_mode_tran.npy", final_mode_tran_arr)
np.save("unit_cell_param_sweep_mode_phase.npy", final_mode_phase_arr)
plt.figure()
plt.subplot(1,2,1)
plt.pcolormesh(
mode_wvl,
diameters,
final_mode_tran_arr,
cmap='hot_r',
shading='gouraud',
vmin=0,
vmax=final_mode_tran_arr.max(),
)
plt.axis([wvl_min, wvl_max, diameters[0], diameters[-1]])
plt.xlabel("Wavelength (um)")
plt.xticks([t for t in np.linspace(wvl_min, wvl_max, 3)])
plt.ylabel("Pillar Diameter (um)")
plt.title("Transmission")
cbar=plt.colorbar()
cbar.set_ticks([t for t in np.arange(0, 1.2, 0.2)])
plt.subplot(1,2,2)
plt.pcolormesh(
mode_wvl,
diameters,
final_mode_phase_arr,
cmap='RdBu',
shading='gouraud',
vmin=final_mode_phase_arr.min(),
vmax=final_mode_phase_arr.max(),
)
plt.axis([wvl_min, wvl_max, diameters[0], diameters[-1]])
plt.xlabel("Wavelength (um)")
plt.xticks([t for t in np.linspace(wvl_min, wvl_max, 3)])
plt.ylabel("Pillar Diameter (um)")
plt.title("phase")
cbar=plt.colorbar()
cbar.set_ticks([t for t in np.arange(-3, 4, 1)])
plt.subplots_adjust(wspace=0.5)
plt.savefig("unit_cell_param_sweep.pdf")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+42
View File
@@ -0,0 +1,42 @@
import numpy as np
import matplotlib.pyplot as plt
#import the parameter sweep arrays for the central wavelength
diameters = np.load("unit_cell/unit_cell_center_1550_h900nm_res50_wvl_diameters.npy")
print(diameters)
mode_wvl = np.load("unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_wvl.npy")
mode_tran_arr = np.load("unit_cell/unit_cell_center_1550_h900nm_res50_wvl_mode_tran.npy")
print(mode_tran_arr)
mode_phase_arr = np.load("unit_cell/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("unit_cell/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 (nm)")
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 (nm)")
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/unit_cell_param_sweep_lineplot.pdf")