implemented and did some testing about applying a conic filter on phase masks. This should smooth out outliers at the cost of a possible reduction of total overlap efficiency. Several example devices have been added for testing purposes.

This commit is contained in:
Edoardo Vergerio
2026-09-08 16:56:56 +02:00
parent b4e79330b1
commit 45492233d4
5 changed files with 250 additions and 108 deletions
+43 -41
View File
@@ -1,9 +1,9 @@
import autograd.numpy as np import autograd.numpy as npa
from numpy.typing import ArrayLike as ArrayLikeType from numpy.typing import ArrayLike as ArrayLikeType
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
filter_radius_um = 1 filter_radius_um = 1
def _centered(arr: np.ndarray, newshape: ArrayLikeType) -> np.ndarray: def _centered(arr: npa.ndarray, newshape: ArrayLikeType) -> npa.ndarray:
"""Formats the output of an FFT to center the zero-frequency component. """Formats the output of an FFT to center the zero-frequency component.
A helper function borrowed from SciPy: A helper function borrowed from SciPy:
@@ -17,15 +17,15 @@ def _centered(arr: np.ndarray, newshape: ArrayLikeType) -> np.ndarray:
Returns: Returns:
The input array with the zero-frequency component as the central element. The input array with the zero-frequency component as the central element.
""" """
newshape = np.asarray(newshape) newshape = npa.asarray(newshape)
currshape = np.array(arr.shape) currshape = npa.array(arr.shape)
startind = (currshape - newshape) // 2 startind = (currshape - newshape) // 2
endind = startind + newshape endind = startind + newshape
myslice = [slice(startind[k], endind[k]) for k in range(len(endind))] myslice = [slice(startind[k], endind[k]) for k in range(len(endind))]
return arr[tuple(myslice)] return arr[tuple(myslice)]
def _edge_pad(arr: np.ndarray, pad: np.ndarray) -> np.ndarray: def _edge_pad(arr: npa.ndarray, pad: npa.ndarray) -> npa.ndarray:
"""Border-pads the edges of an array. """Border-pads the edges of an array.
Used to preprocess the design weights prior to convolution with the filter. Used to preprocess the design weights prior to convolution with the filter.
@@ -42,36 +42,36 @@ def _edge_pad(arr: np.ndarray, pad: np.ndarray) -> np.ndarray:
A 2d array with border padding. A 2d array with border padding.
""" """
# fill sides # fill sides
left = np.tile(arr[0, :], (pad[0][0], 1)) left = npa.tile(arr[0, :], (pad[0][0], 1))
right = np.tile(arr[-1, :], (pad[0][1], 1)) right = npa.tile(arr[-1, :], (pad[0][1], 1))
top = np.tile(arr[:, 0], (pad[1][0], 1)).transpose() top = npa.tile(arr[:, 0], (pad[1][0], 1)).transpose()
bottom = np.tile(arr[:, -1], (pad[1][1], 1)).transpose() bottom = npa.tile(arr[:, -1], (pad[1][1], 1)).transpose()
# fill corners # fill corners
top_left = np.tile(arr[0, 0], (pad[0][0], pad[1][0])) top_left = npa.tile(arr[0, 0], (pad[0][0], pad[1][0]))
top_right = np.tile(arr[-1, 0], (pad[0][1], pad[1][0])) top_right = npa.tile(arr[-1, 0], (pad[0][1], pad[1][0]))
bottom_left = np.tile(arr[0, -1], (pad[0][0], pad[1][1])) bottom_left = npa.tile(arr[0, -1], (pad[0][0], pad[1][1]))
bottom_right = np.tile(arr[-1, -1], (pad[0][1], pad[1][1])) bottom_right = npa.tile(arr[-1, -1], (pad[0][1], pad[1][1]))
if pad[0][0] > 0 and pad[0][1] > 0 and pad[1][0] > 0 and pad[1][1] > 0: if pad[0][0] > 0 and pad[0][1] > 0 and pad[1][0] > 0 and pad[1][1] > 0:
return np.concatenate( return npa.concatenate(
( (
np.concatenate((top_left, top, top_right)), npa.concatenate((top_left, top, top_right)),
np.concatenate((left, arr, right)), npa.concatenate((left, arr, right)),
np.concatenate((bottom_left, bottom, bottom_right)), npa.concatenate((bottom_left, bottom, bottom_right)),
), ),
axis=1, axis=1,
) )
elif pad[0][0] == 0 and pad[0][1] == 0 and pad[1][0] > 0 and pad[1][1] > 0: elif pad[0][0] == 0 and pad[0][1] == 0 and pad[1][0] > 0 and pad[1][1] > 0:
return np.concatenate((top, arr, bottom), axis=1) return npa.concatenate((top, arr, bottom), axis=1)
elif pad[0][0] > 0 and pad[0][1] > 0 and pad[1][0] == 0 and pad[1][1] == 0: elif pad[0][0] > 0 and pad[0][1] > 0 and pad[1][0] == 0 and pad[1][1] == 0:
return np.concatenate((left, arr, right), axis=0) return npa.concatenate((left, arr, right), axis=0)
elif pad[0][0] == 0 and pad[0][1] == 0 and pad[1][0] == 0 and pad[1][1] == 0: elif pad[0][0] == 0 and pad[0][1] == 0 and pad[1][0] == 0 and pad[1][1] == 0:
return arr return arr
else: else:
raise ValueError("At least one of the padding numbers is invalid.") raise ValueError("At least one of the padding numbers is invalid.")
def _quarter_to_full_kernel(arr: np.ndarray, pad_to: np.ndarray) -> np.ndarray: def _quarter_to_full_kernel(arr: npa.ndarray, pad_to: npa.ndarray) -> npa.ndarray:
"""Constructs the full kernel from its nonnegative quadrant. """Constructs the full kernel from its nonnegative quadrant.
Args: Args:
@@ -83,24 +83,24 @@ def _quarter_to_full_kernel(arr: np.ndarray, pad_to: np.ndarray) -> np.ndarray:
Returns: Returns:
The complete kernel. The complete kernel.
""" """
pad_size = pad_to - 2 * np.array(arr.shape) + 1 pad_size = pad_to - 2 * npa.array(arr.shape) + 1
top = np.zeros((pad_size[0], arr.shape[1])) top = npa.zeros((pad_size[0], arr.shape[1]))
bottom = np.zeros((pad_size[0], arr.shape[1] - 1)) bottom = npa.zeros((pad_size[0], arr.shape[1] - 1))
middle = np.zeros((pad_to[0], pad_size[1])) middle = npa.zeros((pad_to[0], pad_size[1]))
top_left = arr[:, :] top_left = arr[:, :]
top_right = np.flipud(arr[1:, :]) top_right = npa.flipud(arr[1:, :])
bottom_left = np.fliplr(arr[:, 1:]) bottom_left = npa.fliplr(arr[:, 1:])
bottom_right = np.flipud( bottom_right = npa.flipud(
np.fliplr(arr[1:, 1:]) npa.fliplr(arr[1:, 1:])
) # equivalent to flip, but flip is incompatible with autograd ) # equivalent to flip, but flip is incompatible with autograd
return np.concatenate( return npa.concatenate(
( (
np.concatenate((top_left, top, top_right)), npa.concatenate((top_left, top, top_right)),
middle, middle,
np.concatenate((bottom_left, bottom, bottom_right)), npa.concatenate((bottom_left, bottom, bottom_right)),
), ),
axis=1, axis=1,
) )
@@ -113,34 +113,36 @@ def conic_filter(phase_mask,radius,space_grid):
phase_mask: the input phase mask to be filtered (2D) phase_mask: the input phase mask to be filtered (2D)
radius: the radius of the conic filter in micrometers radius: the radius of the conic filter in micrometers
space_grid: an object containing the size and resolution of the phase mask space_grid: an object containing the size and resolution of the phase mask
Returns: Returns:
The filtered phase mask (2D) The filtered phase mask (2D)
''' '''
Nx = space_grid.get_Nx() Nx = space_grid.get_Nx()
Ny = space_grid.get_Ny() Ny = space_grid.get_Ny()
xv = np.arange(0, Nx//2-1) * (space_grid.get_resolution()) #positive quadrant of the filter
yv = np.arange(0, Ny//2-1) * (space_grid.get_resolution()) xv = npa.arange(0, Nx//2+1) * (space_grid.get_resolution())
Xv, Yv = np.meshgrid(xv, yv) yv = npa.arange(0, Ny//2+1) * (space_grid.get_resolution())
#note that the filter's center is at (0,0)
Xv, Yv = npa.meshgrid(xv, yv)
h = np.where(Xv**2 + Yv**2 < radius**2, (1 - np.sqrt(abs(Xv**2 + Yv**2)) / radius),0) 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 * np.array([Nx, Ny])) h = _quarter_to_full_kernel(h, 3 * npa.array([Nx, Ny]))
h = h / np.sum(h) h = h / npa.sum(h)
#make a plot of the filter kernel #make a plot of the filter kernel
H = np.fft.fft2(h) H = npa.fft.fft2(h)
phase_mask_2d = phase_mask.reshape(Nx,Ny) phase_mask_2d = phase_mask.reshape(Nx,Ny)
phase_mask_padded = _edge_pad(phase_mask_2d, ((Nx, Nx), (Ny, Ny))) phase_mask_padded = _edge_pad(phase_mask_2d, ((Nx, Nx), (Ny, Ny)))
phase_mask_fft = np.fft.fft2(phase_mask_padded) phase_mask_fft = npa.fft.fft2(phase_mask_padded)
Y = phase_mask_fft * H Y = phase_mask_fft * H
y = _centered( y = _centered(
np.real(np.fft.ifft2(Y)), npa.real(npa.fft.ifft2(Y)),
(Nx, Ny) (Nx, Ny)
) )
+105 -53
View File
@@ -5,6 +5,7 @@ import os
import pyfftw import pyfftw
import nlopt import nlopt
from autograd import tensor_jacobian_product, grad as gd from autograd import tensor_jacobian_product, grad as gd
import autograd.numpy as npa
from filters import conic_filter from filters import conic_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:
@@ -62,6 +63,8 @@ def make_1Dplot_of(x_axis,y_axis,plot_zoom_x,save_name="plot_1D",) -> None:
plt.close() plt.close()
class SpaceGrid: class SpaceGrid:
'''
'''
def __init__(self, side_pixels: int, unit_cell_pitch: float, prop_distances: np.ndarray, materials_refr_index: np.ndarray, xy_symmetry=False): def __init__(self, side_pixels: int, unit_cell_pitch: float, prop_distances: np.ndarray, materials_refr_index: np.ndarray, xy_symmetry=False):
self.Nx = side_pixels#for the moment we consider only square grids self.Nx = side_pixels#for the moment we consider only square grids
self.Ny = side_pixels self.Ny = side_pixels
@@ -346,7 +349,7 @@ class Simulation:
pyfftw.interfaces.cache.disable() pyfftw.interfaces.cache.disable()
pyfftw.config.PLANNER_EFFORT = planner_effort pyfftw.config.PLANNER_EFFORT = planner_effort
def set_optimization(self, opt_param_min, opt_param_max, ms_max_avail_diameter, phase_mask_init_size, opt_max_eval, conic_filter_radius, results_while_solving=True, custom_mask = None): def set_optimization(self, opt_param_min, opt_param_max, ms_max_avail_diameter, phase_mask_init_size, opt_max_eval, conic_filter_radius = None, results_while_solving=True, custom_mask = None):
''' '''
Sets the optimization parameters for the simulation. Sets the optimization parameters for the simulation.
Args: Args:
@@ -362,24 +365,33 @@ class Simulation:
self.opt_max_eval = opt_max_eval self.opt_max_eval = opt_max_eval
self.results_while_solving = results_while_solving self.results_while_solving = results_while_solving
self.custom_mask = custom_mask #future: one might be able to directly pass a custom mask self.custom_mask = custom_mask #future: one might be able to directly pass a custom mask
if conic_filter_radius is None:
self.use_filtering = False
else:
self.use_filtering = True
self.filter_radius = None
if self.use_filtering:
self.filter_radius = conic_filter_radius self.filter_radius = conic_filter_radius
filtered_phase_mask_1 = self._phase_filter(self.phase_mask[:self.space_grid.get_S()]) 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_2 = self._phase_filter(self.phase_mask[self.space_grid.get_S():])
self.filtered_phase_mask = np.concatenate((filtered_phase_mask_1, filtered_phase_mask_2)) self.filtered_phase_mask = np.concatenate((filtered_phase_mask_1, filtered_phase_mask_2))
else:
self.filtered_phase_mask = self.phase_mask
mask_upper_bounds = self.norm_phase_max * np.where( self.space_grid.get_X().flatten()**2 + self.space_grid.get_Y().flatten()**2 < (self.ms_max_avail_diameter/2)**2, 1, 0) mask_upper_bounds = self.norm_phase_max * np.where( self.space_grid.get_X().flatten()**2 + self.space_grid.get_Y().flatten()**2 < (self.ms_max_avail_diameter/2)**2, 1, 0)
mask_upper_bounds = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0) mask_upper_bounds_full = np.concatenate((mask_upper_bounds, mask_upper_bounds),axis=0)
mask_lower_bounds = np.zeros_like(mask_upper_bounds) mask_lower_bounds_full = np.zeros_like(mask_upper_bounds_full)
if self.space_grid.xy_symmetry: if self.space_grid.xy_symmetry:
mask_upper_bounds_symm = mask_upper_bounds.reshape((self.space_grid.get_Nx(),self.space_grid.get_Ny()))[:self.space_grid.get_nx()+1, :self.space_grid.get_ny()+1].flatten() mask_upper_bounds_symm = mask_upper_bounds.reshape((self.space_grid.get_Nx(),self.space_grid.get_Ny()))[:self.space_grid.get_nx()+1, :self.space_grid.get_ny()+1].flatten()
mask_upper_bounds_symm = np.concatenate((mask_upper_bounds_symm, mask_upper_bounds_symm),axis=0) mask_upper_bounds_symm_full = np.concatenate((mask_upper_bounds_symm, mask_upper_bounds_symm),axis=0)
mask_lower_bounds_symm = np.zeros_like(mask_upper_bounds_symm) mask_lower_bounds_symm_full = np.zeros_like(mask_upper_bounds_symm_full)
self.chosen_mask_upper_bounds = mask_upper_bounds_symm self.chosen_mask_upper_bounds = mask_upper_bounds_symm_full
self.chosen_mask_lower_bounds = mask_lower_bounds_symm self.chosen_mask_lower_bounds = mask_lower_bounds_symm_full
else: else:
self.chosen_mask_upper_bounds = mask_upper_bounds self.chosen_mask_upper_bounds = mask_upper_bounds_full
self.chosen_mask_lower_bounds = mask_lower_bounds self.chosen_mask_lower_bounds = mask_lower_bounds_full
def get_sources(self): def get_sources(self):
@@ -391,6 +403,9 @@ class Simulation:
def get_space_grid(self): def get_space_grid(self):
return self.space_grid return self.space_grid
def get_ms_max_avail_diameter(self):
return self.ms_max_avail_diameter
def get_central_wavelength(self): def get_central_wavelength(self):
return self.central_wavelength return self.central_wavelength
@@ -453,26 +468,37 @@ class Simulation:
''' '''
Initializes the phase masks for the simulation. Initializes the phase masks for the simulation.
This method should be called once, before iterating over the targets. This method should be called once, before iterating over the targets.
If multiple targets are present in this simulation instance, the first initialization uses the first target If multiple source/targets are present in this simulation instance, the mask
is locally initialized around each source and target.
''' '''
source_to_initialize = self.sources[0]
target_to_initialize = self.targets[0]
source_Efield = source_to_initialize.get_field(flatten=True) source_Efields = [source.get_field(flatten=True) for source in self.sources]
source_center = source_to_initialize.get_center() source_centers = [source.get_center() for source in self.sources]
target_Efield = target_to_initialize.get_field(flatten=True)
target_center = target_to_initialize.get_center() target_Efields = [target.get_field(flatten=True) for target in self.targets]
target_centers = [target.get_center() for target in self.targets]
self.w_norm = np.zeros((2*self.space_grid.get_S())) self.w_norm = np.zeros((2*self.space_grid.get_S()))
circular_mask_target = self._build_circular_mask(target_center, self.phase_mask_init_size) circular_mask_source = [self._build_circular_mask(source_center, self.phase_mask_init_size) for source_center in source_centers]
circular_mask_source = self._build_circular_mask(source_center, self.phase_mask_init_size) circular_mask_target = [self._build_circular_mask(target_center, self.phase_mask_init_size) for target_center in target_centers]
source_pattern_phase = (np.angle(source_Efield) + 2 * np.pi) % (2 * np.pi) * circular_mask_source #make sure the phase is between 0 and 2pi source_pattern_phase = [(np.angle(source_Efields[i]) + 2 * np.pi) % (2 * np.pi) * circular_mask_source[i] for i in range(len(source_Efields))] #make sure the phase is between 0 and 2pi
target_pattern_phase = (np.angle(target_Efield) + 2 * np.pi) % (2 * np.pi) * circular_mask_target #make sure the phase is between 0 and 2pi target_pattern_phase = [(np.angle(target_Efields[i]) + 2 * np.pi) % (2 * np.pi) * circular_mask_target[i] for i in range(len(target_Efields))] #make sure the phase is between 0 and 2pi
self.w_norm[:self.space_grid.get_S()] += source_pattern_phase / (2 * np.pi) #normalize the source phase to be between 0 and 1 #initialize mask on source side
self.w_norm[self.space_grid.get_S():] += target_pattern_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1 cumulated_source_phase = np.zeros_like(self.w_norm[:self.space_grid.get_S()])
for _, source_phase in enumerate(source_pattern_phase):
cumulated_source_phase += source_phase / (2 * np.pi) #normalize the source phase to be between 0 and 1
self.w_norm[:self.space_grid.get_S()] = ((cumulated_source_phase + 2*np.pi) % (2*np.pi)) / (2*np.pi)
#initialize mask on target side
cumulated_target_phase = np.zeros_like(self.w_norm[self.space_grid.get_S():])
for _, target_phase in enumerate(target_pattern_phase):
cumulated_target_phase += target_phase / (2 * np.pi) #normalize the target phase to be between 0 and 1
self.w_norm[self.space_grid.get_S():] = ((cumulated_target_phase + 2*np.pi) % (2*np.pi)) / (2*np.pi)
if self.space_grid.xy_symmetry: if self.space_grid.xy_symmetry:
w_norm_symm_1 = self._symmetrize(self.w_norm[:self.space_grid.get_S()]) w_norm_symm_1 = self._symmetrize(self.w_norm[:self.space_grid.get_S()])
@@ -482,24 +508,35 @@ class Simulation:
self.phase_mask[:] = self._phase_given_w(self.w_norm) self.phase_mask[:] = self._phase_given_w(self.w_norm)
#make a plot of the initialized phase mask def _phase_filter(self, phase_mask: np.ndarray) -> np.ndarray:
plt.imshow(self.phase_mask[:self.space_grid.get_S()].reshape((self.space_grid.get_Nx(), self.space_grid.get_Ny())), cmap='gray') '''Applies a conic filter on the phase mask. The filtering action is equivalent to a local weighted averaging of the phase.
plt.title("Initialized Phase Mask") Note that due to the circular behavior of phase, a direct averaging of phase values can lead to incorrect results.
plt.colorbar() The following code averages the sine and cosine components separately and then reconstructs the phase mask through the arctangent.
plt.savefig("results/initialized_phase_mask_1.pdf") A practical example might help: if the phase mask contains values close to 0 and 2π, a direct averaging would incorrectly yield a value around π.
plt.imshow(self.phase_mask[self.space_grid.get_S():].reshape((self.space_grid.get_Nx(), self.space_grid.get_Ny())), cmap='gray') By averaging the sine and cosine components separately, the correct average near 0 (or 2π) is obtained.
plt.title("Initialized Phase Mask")
plt.colorbar()
plt.savefig("results/initialized_phase_mask_2.pdf")
def _phase_filter(self, phase_mask): Args:
'''Applies a conic filter on the phase mask''' phase_mask (ndarray): The input phase mask to be filtered.
phase_mask_filtered = conic_filter(
phase_mask, Returns:
ndarray: The filtered phase mask.
'''
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.filter_radius,
self.space_grid, self.space_grid,
) )
return phase_mask_filtered.flatten()
phase_mask_filtered_sin = conic_filter(
phase_mask_sin,
self.filter_radius,
self.space_grid,
)
return npa.arctan2(phase_mask_filtered_sin, phase_mask_filtered_cos)
def _forward_propagate(self, source_object: Source): def _forward_propagate(self, source_object: Source):
@@ -533,6 +570,11 @@ class Simulation:
def _reduce_symmetrized_gradient(self, g_full): def _reduce_symmetrized_gradient(self, g_full):
'''
Utility function that takes a spatial full size gradient and reduces it
to a smaller, symmetrized representation. In other words, it combines
the contributions from the other quadrants into the selected quadrant.
'''
g_2d = g_full.reshape((self.space_grid.get_Nx(), self.space_grid.get_Ny())) g_2d = g_full.reshape((self.space_grid.get_Nx(), self.space_grid.get_Ny()))
third = g_2d[self.space_grid.get_nx()+1:,:self.space_grid.get_ny()+1] third = g_2d[self.space_grid.get_nx()+1:,:self.space_grid.get_ny()+1]
@@ -612,12 +654,15 @@ class Simulation:
self.w_norm[:] = all_ms self.w_norm[:] = all_ms
self.phase_mask[:] = self._phase_given_w(self.w_norm) self.phase_mask[:] = self._phase_given_w(self.w_norm)
phase_mask_1 = self.phase_mask[:self.space_grid.get_S()]
phase_mask_2 = self.phase_mask[self.space_grid.get_S():]
if self.use_filtering:
filtered_phase_mask_1 = self._phase_filter(phase_mask_1)
filtered_phase_mask_2 = self._phase_filter(phase_mask_2)
self.filtered_phase_mask[:] = npa.concatenate((filtered_phase_mask_1, filtered_phase_mask_2))
filtered_phase_mask_1 = self._phase_filter(self.phase_mask[:self.space_grid.get_S()]) else:
filtered_phase_mask_2 = self._phase_filter(self.phase_mask[self.space_grid.get_S():]) self.filtered_phase_mask[:] = self.phase_mask
self.filtered_phase_mask[:] = np.concatenate((filtered_phase_mask_1, filtered_phase_mask_2))
#after the phase mask is set, we test the same mask across all targets #after the phase mask is set, we test the same mask across all targets
C_all_wavelengths = [] C_all_wavelengths = []
@@ -625,19 +670,18 @@ class Simulation:
for target in self.targets: for target in self.targets:
C, full_grad1, full_grad2 = self._value_and_grad(target) C, full_grad_1, full_grad_2 = self._value_and_grad(target)
C_all_wavelengths.append(C) C_all_wavelengths.append(C)
if self.space_grid.xy_symmetry: jac_grad_1 = tensor_jacobian_product(self._phase_filter)(phase_mask_1, full_grad_1)
grad_partial_1 = self._reduce_symmetrized_gradient(full_grad1) jac_grad_2 = tensor_jacobian_product(self._phase_filter)(phase_mask_2, full_grad_2)
grad_partial_2 = self._reduce_symmetrized_gradient(full_grad2)
else:
grad_partial_1 = full_grad1
grad_partial_2 = full_grad2
jac_grad_1 = tensor_jacobian_product(self._phase_filter)(self.phase_mask[:self.space_grid.get_S()], grad_partial_1) if self.space_grid.xy_symmetry:
jac_grad_2 = tensor_jacobian_product(self._phase_filter)(self.phase_mask[self.space_grid.get_S():], grad_partial_2) jac_grad_1_symm = self._reduce_symmetrized_gradient(jac_grad_1)
jac_grad_2_symm = self._reduce_symmetrized_gradient(jac_grad_2)
jac_grad = np.concatenate((jac_grad_1_symm, jac_grad_2_symm), axis=0)
else:
jac_grad = np.concatenate((jac_grad_1, jac_grad_2),axis=0) jac_grad = np.concatenate((jac_grad_1, jac_grad_2),axis=0)
grad_partial[:] += jac_grad grad_partial[:] += jac_grad
@@ -683,15 +727,23 @@ class Simulation:
output_field.append(self.intermediate_fields[-1]) output_field.append(self.intermediate_fields[-1])
np.save(f"{self.results_folder}/output_fields_{i}.npy", output_field[-1]) np.save(f"{self.results_folder}/output_fields_{i}.npy", output_field[-1])
make_2Dplot_of(output_field[-1], self.space_grid.get_X(), self.space_grid.get_Y(), plot_zoom_x=60, plot_zoom_y=60, choose_quantity="amplitude", save_name=f"{self.results_folder}/output_fields_{i}_plot") make_2Dplot_of(output_field[-1], self.space_grid.get_X(), self.space_grid.get_Y(), plot_zoom_x=self.get_ms_max_avail_diameter()/2, plot_zoom_y=self.get_ms_max_avail_diameter()/2, choose_quantity="amplitude", save_name=f"{self.results_folder}/output_fields_{i}_plot")
make_2Dplot_of(output_field[-1], self.space_grid.get_X(), self.space_grid.get_Y(), plot_zoom_x=self.get_ms_max_avail_diameter()/2, plot_zoom_y=self.get_ms_max_avail_diameter()/2, choose_quantity="phase", save_name=f"{self.results_folder}/output_fields_{i}_phase_plot")
#phase masks #phase masks
phase_mask_1 = self._phase_given_w(self.w_norm[:self.space_grid.get_S()]).reshape((self.space_grid.get_Nx(), self.space_grid.get_Ny())) phase_mask_1 = self._phase_given_w(self.w_norm[:self.space_grid.get_S()]).reshape((self.space_grid.get_Nx(), self.space_grid.get_Ny()))
phase_mask_2 = self._phase_given_w(self.w_norm[self.space_grid.get_S():]).reshape((self.space_grid.get_Nx(), self.space_grid.get_Ny())) phase_mask_2 = self._phase_given_w(self.w_norm[self.space_grid.get_S():]).reshape((self.space_grid.get_Nx(), self.space_grid.get_Ny()))
make_2Dplot_of(phase_mask_1, self.space_grid.get_X(), self.space_grid.get_Y(), plot_zoom_x=60, plot_zoom_y=60, choose_quantity="phase_mask", save_name=f"{self.results_folder}/phase_mask_1_plot") filtered_phase_mask_1 = self._phase_filter(phase_mask_1)
make_2Dplot_of(phase_mask_2, self.space_grid.get_X(), self.space_grid.get_Y(), plot_zoom_x=60, plot_zoom_y=60, choose_quantity="phase_mask", save_name=f"{self.results_folder}/phase_mask_2_plot") filtered_phase_mask_2 = self._phase_filter(phase_mask_2)
make_2Dplot_of(phase_mask_1, self.space_grid.get_X(), self.space_grid.get_Y(), plot_zoom_x=self.get_ms_max_avail_diameter()/2, plot_zoom_y=self.get_ms_max_avail_diameter()/2, choose_quantity="phase_mask", save_name=f"{self.results_folder}/phase_mask_1_plot")
make_2Dplot_of(phase_mask_2, self.space_grid.get_X(), self.space_grid.get_Y(), plot_zoom_x=self.get_ms_max_avail_diameter()/2, plot_zoom_y=self.get_ms_max_avail_diameter()/2, choose_quantity="phase_mask", save_name=f"{self.results_folder}/phase_mask_2_plot")
make_2Dplot_of(filtered_phase_mask_1, self.space_grid.get_X(), self.space_grid.get_Y(), plot_zoom_x=self.get_ms_max_avail_diameter()/2, plot_zoom_y=self.get_ms_max_avail_diameter()/2, choose_quantity="phase_mask", save_name=f"{self.results_folder}/filtered_phase_mask_1_plot")
make_2Dplot_of(filtered_phase_mask_2, self.space_grid.get_X(), self.space_grid.get_Y(), plot_zoom_x=self.get_ms_max_avail_diameter()/2, plot_zoom_y=self.get_ms_max_avail_diameter()/2, choose_quantity="phase_mask", save_name=f"{self.results_folder}/filtered_phase_mask_2_plot")
np.save(f"{self.results_folder}/optimized_phase_mask_1.npy", phase_mask_1) np.save(f"{self.results_folder}/optimized_phase_mask_1.npy", phase_mask_1)
np.save(f"{self.results_folder}/optimized_phase_mask_2.npy", phase_mask_2) np.save(f"{self.results_folder}/optimized_phase_mask_2.npy", phase_mask_2)
np.save(f"{self.results_folder}/filtered_phase_mask_1.npy", filtered_phase_mask_1)
np.save(f"{self.results_folder}/filtered_phase_mask_2.npy", filtered_phase_mask_2)
def get_best_opt_val(self): def get_best_opt_val(self):
if self.is_optimized_flag: if self.is_optimized_flag:
@@ -1,6 +1,8 @@
import mpid as pid import mpid as pid
import numpy as np import numpy as np
import os
if not os.path.exists("results"):
os.makedirs("results")
#Space grid definition #Space grid definition
N = 1025 N = 1025
unit_cell_pitch = 700e-9 unit_cell_pitch = 700e-9
@@ -42,16 +44,16 @@ sources = [pid.Source(input_field=gaussian_field_normalized,
sources[0].plot2D(plot_zoom_x=38,plot_zoom_y=38,choose_quantity='amplitude', save_name='results/input_field') sources[0].plot2D(plot_zoom_x=38,plot_zoom_y=38,choose_quantity='amplitude', save_name='results/input_field')
#define targets #define targets
#import field data from files and create target objects #import field data from files and create target objects
target_files = ["fiber_modes/LP21_field_distribution_1025.npy"] target_files = ["fiber_modes/LP21_field_distribution_lda_1550_N_1025.npy",]
target_fields = [np.load(file) for file in target_files] target_fields = [np.load(file) for file in target_files]
norm_target_fields = [field / np.sqrt(np.sum(np.abs(field)**2)) for field in target_fields]
targets = [pid.Target(input_field=norm_target_fields[i], targets = [pid.Target(input_field=target_fields[i],
associated_source=sources[i], associated_source=sources[i],
wavelength=wvl, wavelength=wvl,
space_grid=space_grid, space_grid=space_grid,
wvl_unit_measure=space_grid.nm) for i, (file, wvl) in enumerate(zip(target_files, wavelengths_to_test_nm))] wvl_unit_measure=space_grid.nm,
field_norm = True) for i, (file, wvl) in enumerate(zip(target_files, wavelengths_to_test_nm))]
targets[0].plot2D(plot_zoom_x=38,plot_zoom_y=38,choose_quantity='amplitude', save_name='results/target_field') targets[0].plot2D(plot_zoom_x=38,plot_zoom_y=38,choose_quantity='amplitude', save_name='results/target_field')
@@ -70,6 +72,7 @@ simulation.set_optimization(
ms_max_avail_diameter=ms_max_available_diameter, ms_max_avail_diameter=ms_max_available_diameter,
phase_mask_init_size=output_fiber_core_diameter/3, phase_mask_init_size=output_fiber_core_diameter/3,
opt_max_eval=opt_max_eval, opt_max_eval=opt_max_eval,
conic_filter_radius=1.5e-6,
) )
simulation.run() simulation.run()
@@ -56,7 +56,6 @@ target2 = pid.Target(input_field='fiber_modes/LP01_field_distribution_lda_1550_N
field_norm=True) field_norm=True)
#make a plot of sources and targets #make a plot of sources and targets
target.plot2D(plot_zoom_x=100, plot_zoom_y=100, choose_quantity="amplitude", save_name="results/target_plot") target.plot2D(plot_zoom_x=100, plot_zoom_y=100, choose_quantity="amplitude", save_name="results/target_plot")
target2.plot2D(plot_zoom_x=100, plot_zoom_y=100, choose_quantity="amplitude", save_name="results/target2_plot") target2.plot2D(plot_zoom_x=100, plot_zoom_y=100, choose_quantity="amplitude", save_name="results/target2_plot")
source1.plot2D(plot_zoom_x=100, plot_zoom_y=100, choose_quantity="amplitude", save_name="results/source1_plot") source1.plot2D(plot_zoom_x=100, plot_zoom_y=100, choose_quantity="amplitude", save_name="results/source1_plot")
+86
View File
@@ -0,0 +1,86 @@
import mpid as pid
import numpy as np
import os
if not os.path.exists("results"):
os.makedirs("results")
#Space grid definition
N = 1025
unit_cell_pitch = 700e-9
n0 = 1.45
n1 = 1.0
n2 = 1.45
mat_refr_index = np.array([n0, n1, n2])
d0 = 476e-6 #propagation distance: source - MS1 [m]
d1 = 510e-6 #propagation distance: MS1 - MS2 [m]
d2 = d0 #propagation distance: MS2 - target [m]
d = np.array([d0, d1, d2])
ms_max_available_diameter = 122e-6 #[um]
output_fiber_core_diameter = 19e-6 #[um]
opt_max_eval = 140
space_grid = pid.SpaceGrid(
side_pixels=N,
unit_cell_pitch = unit_cell_pitch,
prop_distances=d,
materials_refr_index=mat_refr_index,
)
wavelength = 1.55e-6
#define the sources
source1 = pid.Source(input_field='fiber_modes/LP11_field_distribution_lda_1550_N_1025.npy',
wavelength=wavelength,
space_grid=space_grid,
field_norm=True)
source2 = pid.Source(input_field='fiber_modes/LP21_field_distribution_lda_1550_N_1025.npy',
wavelength=wavelength,
space_grid=space_grid,
field_norm=True)
target = pid.Target(input_field='fiber_modes/LP11_field_distribution_lda_1550_N_1025.npy',
center=(20e-6,0),
associated_source=source1,
wavelength=wavelength,
space_grid=space_grid,
field_norm=True)
target2 = pid.Target(input_field='fiber_modes/LP21_field_distribution_lda_1550_N_1025.npy',
center=(-20e-6,0),
associated_source=source2,
wavelength=wavelength,
space_grid=space_grid,
field_norm=True)
#make a plot of sources and targets
target.plot2D(plot_zoom_x=100, plot_zoom_y=100, choose_quantity="amplitude", save_name="results/target_plot")
target2.plot2D(plot_zoom_x=100, plot_zoom_y=100, choose_quantity="amplitude", save_name="results/target2_plot")
source1.plot2D(plot_zoom_x=100, plot_zoom_y=100, choose_quantity="amplitude", save_name="results/source1_plot")
source2.plot2D(plot_zoom_x=100, plot_zoom_y=100, choose_quantity="amplitude", save_name="results/source2_plot")
sim = pid.Simulation(
sources=[source1, source2],
targets=[target, target2],
central_wavelength=wavelength,
space_grid=space_grid,
n_cpu=1
)
opt_param_min = 0
opt_param_max = 1
sim.set_optimization(opt_param_min=opt_param_min,
opt_param_max=opt_param_max,
ms_max_avail_diameter=ms_max_available_diameter,
phase_mask_init_size=output_fiber_core_diameter,
opt_max_eval=opt_max_eval,
conic_filter_radius=1.5e-6)
sim.run()
best_opt_val, distances = sim.get_best_opt_and_distances()
print("Best optimization value:", best_opt_val)
print("Distances:", distances)