mirror of
https://github.com/edoverg/MPID.git
synced 2026-10-14 23:26:38 +00:00
modified mpid to use filtering on the phase mask parameters. Automatic differentiation is used to correctly compute gradients. Moreover, the optimization can down be carried out according to different strategies, which can either be: average the objectives, use an epigraph reformulation. The epigraph formulation needs further attention because it does not always leed to good results. A possible solution might be a combination of the two strategies: at the beginning use an average cost function to set up the problem and then switch to an epigraph. This would be like an hot start strategy, since I have noticed the epigraph being not totally capable of handling the problem well if it starts from scratch. On the other hand, the use of the filtering action can even leed to better results than without it, although it is necessary to tune the filter radius. I have seen that even a 1um radius is generally sufficient to remove outliers in the final phase mask, which was the main reason we decided to implement the filter in the first place. In the end, the all defined classes assume we interface to them using dimensions in micrometers. Conversion to meters is handled internally.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
from kiwisolver import Constraint
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
plt.rcParams.update({'font.size': 14})
|
plt.rcParams.update({'font.size': 14})
|
||||||
@@ -64,11 +65,13 @@ def make_1Dplot_of(x_axis,y_axis,plot_zoom_x,save_name="plot_1D",) -> None:
|
|||||||
|
|
||||||
class SpaceGrid:
|
class SpaceGrid:
|
||||||
'''
|
'''
|
||||||
|
Class representing the spatial grid for the simulation.
|
||||||
|
Spatial dimensions are intended to be measured in micrometers [um].
|
||||||
'''
|
'''
|
||||||
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
|
||||||
self.unit_cell_pitch = unit_cell_pitch
|
self.unit_cell_pitch = unit_cell_pitch * 1e-6
|
||||||
self.nx = (self.Nx - 1) // 2
|
self.nx = (self.Nx - 1) // 2
|
||||||
self.ny = (self.Ny - 1) // 2
|
self.ny = (self.Ny - 1) // 2
|
||||||
self.xy_symmetry = xy_symmetry
|
self.xy_symmetry = xy_symmetry
|
||||||
@@ -86,7 +89,7 @@ class SpaceGrid:
|
|||||||
self.X, self.Y = np.meshgrid(self.xs, self.ys)
|
self.X, self.Y = np.meshgrid(self.xs, self.ys)
|
||||||
self.rho = np.sqrt(self.X**2 + self.Y**2)
|
self.rho = np.sqrt(self.X**2 + self.Y**2)
|
||||||
self.sampling_period = self.unit_cell_pitch
|
self.sampling_period = self.unit_cell_pitch
|
||||||
self.d = prop_distances
|
self.d = prop_distances * 1e-6
|
||||||
|
|
||||||
self.nm = 1e-9
|
self.nm = 1e-9
|
||||||
self.m_to_nm = 1e9
|
self.m_to_nm = 1e9
|
||||||
@@ -158,12 +161,11 @@ class SpaceGrid:
|
|||||||
return self.rho
|
return self.rho
|
||||||
|
|
||||||
class Field:
|
class Field:
|
||||||
def __init__(self, input_field: np.ndarray, wavelength: float, space_grid: SpaceGrid, center=(0,0)):
|
def __init__(self, input_field: np.ndarray, wavelength: float, space_grid: SpaceGrid, center: np.ndarray = np.array([0,0])):
|
||||||
self.center = center
|
self.center = center * 1e-6
|
||||||
self.space_grid = space_grid
|
self.space_grid = space_grid
|
||||||
self.input_field = self._shift_field(input_field)
|
self.input_field = self._shift_field(input_field)
|
||||||
self.wavelength = wavelength
|
self.wavelength = wavelength * 1e-6
|
||||||
|
|
||||||
|
|
||||||
if self.input_field.shape != self.space_grid.get_grid_shape():
|
if self.input_field.shape != self.space_grid.get_grid_shape():
|
||||||
raise ValueError("Input field must have the same shape as the space grid it is applied onto.")
|
raise ValueError("Input field must have the same shape as the space grid it is applied onto.")
|
||||||
@@ -195,7 +197,7 @@ class Field:
|
|||||||
save_name)
|
save_name)
|
||||||
|
|
||||||
class Source(Field):
|
class Source(Field):
|
||||||
def __init__(self, input_field: np.ndarray | str, wavelength: float, space_grid: SpaceGrid, wvl_unit_measure = 1, field_norm = False, center = (0,0)):
|
def __init__(self, input_field: np.ndarray | str, wavelength: float, space_grid: SpaceGrid, field_norm = False, center: np.ndarray = np.array([0,0])):
|
||||||
if isinstance(input_field, str):
|
if isinstance(input_field, str):
|
||||||
input_field_to_constr = np.load(input_field)
|
input_field_to_constr = np.load(input_field)
|
||||||
input_field_shape = input_field_to_constr.shape
|
input_field_shape = input_field_to_constr.shape
|
||||||
@@ -206,7 +208,7 @@ class Source(Field):
|
|||||||
if field_norm:
|
if field_norm:
|
||||||
input_field_to_constr = input_field_to_constr / np.sqrt(np.sum(np.abs(input_field_to_constr)**2))
|
input_field_to_constr = input_field_to_constr / np.sqrt(np.sum(np.abs(input_field_to_constr)**2))
|
||||||
|
|
||||||
super().__init__(input_field_to_constr, wavelength * wvl_unit_measure, space_grid, center=center)
|
super().__init__(input_field_to_constr, wavelength, space_grid, center=center)
|
||||||
self.type = "source"
|
self.type = "source"
|
||||||
|
|
||||||
def get_type(self):
|
def get_type(self):
|
||||||
@@ -236,7 +238,12 @@ class GaussianBeam():
|
|||||||
return self.space_grid
|
return self.space_grid
|
||||||
|
|
||||||
class Target(Field):
|
class Target(Field):
|
||||||
def __init__(self, input_field: np.ndarray | str | Field ,associated_source: Source, wavelength: float, space_grid: SpaceGrid, wvl_unit_measure = 1, field_norm = False, center = (0,0)):
|
def __init__(self, input_field: np.ndarray | str | Field ,
|
||||||
|
associated_source: Source,
|
||||||
|
wavelength: float,
|
||||||
|
space_grid: SpaceGrid,
|
||||||
|
field_norm = False,
|
||||||
|
center = np.array([0,0])):
|
||||||
if isinstance(input_field, str):
|
if isinstance(input_field, str):
|
||||||
input_field_to_constr = np.load(input_field)
|
input_field_to_constr = np.load(input_field)
|
||||||
input_field_shape = input_field_to_constr.shape
|
input_field_shape = input_field_to_constr.shape
|
||||||
@@ -247,10 +254,10 @@ class Target(Field):
|
|||||||
if field_norm:
|
if field_norm:
|
||||||
input_field_to_constr = input_field_to_constr / np.sqrt(np.sum(np.abs(input_field_to_constr)**2))
|
input_field_to_constr = input_field_to_constr / np.sqrt(np.sum(np.abs(input_field_to_constr)**2))
|
||||||
|
|
||||||
super().__init__(input_field_to_constr, wavelength * wvl_unit_measure, space_grid, center=center)
|
super().__init__(input_field_to_constr, wavelength, space_grid, center=center)
|
||||||
self.type = "target"
|
self.type = "target"
|
||||||
|
|
||||||
if associated_source.get_wavelength() != wavelength * wvl_unit_measure:
|
if associated_source.get_wavelength() != self.get_wavelength():
|
||||||
raise ValueError("Associated source must have the same wavelength as the target.")
|
raise ValueError("Associated source must have the same wavelength as the target.")
|
||||||
else:
|
else:
|
||||||
self.associated_source = associated_source
|
self.associated_source = associated_source
|
||||||
@@ -297,7 +304,7 @@ class Simulation:
|
|||||||
self.sources = sources
|
self.sources = sources
|
||||||
self.targets = targets
|
self.targets = targets
|
||||||
self.space_grid = space_grid
|
self.space_grid = space_grid
|
||||||
self.central_wavelength = central_wavelength #central_wavelength parameter has little sense to exist. Revise
|
self.central_wavelength = central_wavelength * 1e6 #central_wavelength parameter has little sense to exist. Revise
|
||||||
|
|
||||||
self.operating_wavelength = self.central_wavelength #initially, the operating_wavelength is the central wavelength
|
self.operating_wavelength = self.central_wavelength #initially, the operating_wavelength is the central wavelength
|
||||||
self.lda_vector = [self.operating_wavelength / n for n in self.space_grid.get_materials_refr_index()]
|
self.lda_vector = [self.operating_wavelength / n for n in self.space_grid.get_materials_refr_index()]
|
||||||
@@ -349,22 +356,35 @@ 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 = None, 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,
|
||||||
|
strategy = 'mean',
|
||||||
|
save_results = True):
|
||||||
'''
|
'''
|
||||||
Sets the optimization parameters for the simulation.
|
Sets the optimization parameters for the simulation.
|
||||||
|
Arguments related to physical dimensions are expected to be in micrometers [um].
|
||||||
Args:
|
Args:
|
||||||
optimization_params (dict): A dictionary containing optimization parameters.
|
optimization_params (dict): A dictionary containing optimization parameters.
|
||||||
'''
|
'''
|
||||||
self.is_optimization_set = True
|
self.is_optimization_set = True
|
||||||
#self.optimization_params = optimization_params
|
|
||||||
self.iter_num = [0]
|
self.iter_num = [0]
|
||||||
self.norm_phase_min = opt_param_min
|
self.norm_phase_min = opt_param_min
|
||||||
self.norm_phase_max = opt_param_max
|
self.norm_phase_max = opt_param_max
|
||||||
self.ms_max_avail_diameter = ms_max_avail_diameter
|
self.ms_max_avail_diameter = ms_max_avail_diameter * 1e-6
|
||||||
self.phase_mask_init_size = phase_mask_init_size
|
self.phase_mask_init_size = phase_mask_init_size * 1e-6
|
||||||
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
|
||||||
|
self.strategy = strategy
|
||||||
|
self.save_results = save_results
|
||||||
|
|
||||||
if conic_filter_radius is None:
|
if conic_filter_radius is None:
|
||||||
self.use_filtering = False
|
self.use_filtering = False
|
||||||
else:
|
else:
|
||||||
@@ -372,7 +392,7 @@ class Simulation:
|
|||||||
self.filter_radius = None
|
self.filter_radius = None
|
||||||
|
|
||||||
if self.use_filtering:
|
if self.use_filtering:
|
||||||
self.filter_radius = conic_filter_radius
|
self.filter_radius = conic_filter_radius * 1e-6
|
||||||
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))
|
||||||
@@ -393,6 +413,8 @@ class Simulation:
|
|||||||
self.chosen_mask_upper_bounds = mask_upper_bounds_full
|
self.chosen_mask_upper_bounds = mask_upper_bounds_full
|
||||||
self.chosen_mask_lower_bounds = mask_lower_bounds_full
|
self.chosen_mask_lower_bounds = mask_lower_bounds_full
|
||||||
|
|
||||||
|
self._solver_init()
|
||||||
|
|
||||||
|
|
||||||
def get_sources(self):
|
def get_sources(self):
|
||||||
return self.sources
|
return self.sources
|
||||||
@@ -400,6 +422,9 @@ class Simulation:
|
|||||||
def get_targets(self):
|
def get_targets(self):
|
||||||
return self.targets
|
return self.targets
|
||||||
|
|
||||||
|
def get_num_targets(self):
|
||||||
|
return len(self.targets)
|
||||||
|
|
||||||
def get_space_grid(self):
|
def get_space_grid(self):
|
||||||
return self.space_grid
|
return self.space_grid
|
||||||
|
|
||||||
@@ -539,13 +564,14 @@ class Simulation:
|
|||||||
return npa.arctan2(phase_mask_filtered_sin, phase_mask_filtered_cos)
|
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):
|
||||||
|
#source field
|
||||||
source_to_propagate = source_object.get_field()
|
source_to_propagate = source_object.get_field()
|
||||||
|
|
||||||
self.fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((self.space_grid.get_Nx(), self.space_grid.get_Ny())))
|
self.fft_input_array[:,:] = np.fft.ifftshift(source_to_propagate.reshape((self.space_grid.get_Nx(), self.space_grid.get_Ny())))
|
||||||
self.fft_operator()
|
self.fft_operator()
|
||||||
self.ifft_input_array[:,:] = self.fft_operator.output_array * self.P_nat[0]
|
self.ifft_input_array[:,:] = self.fft_operator.output_array * self.P_nat[0]
|
||||||
self.ifft_operator()
|
self.ifft_operator()
|
||||||
|
#field before MS1
|
||||||
self.intermediate_fields[0] = np.fft.fftshift(self.ifft_operator.output_array)
|
self.intermediate_fields[0] = np.fft.fftshift(self.ifft_operator.output_array)
|
||||||
input_field = self.intermediate_fields[0].flatten()
|
input_field = self.intermediate_fields[0].flatten()
|
||||||
|
|
||||||
@@ -553,6 +579,7 @@ class Simulation:
|
|||||||
self.fft_operator()
|
self.fft_operator()
|
||||||
self.ifft_input_array[:,:] = self.fft_operator.output_array * self.P_nat[1] #element wise product
|
self.ifft_input_array[:,:] = self.fft_operator.output_array * self.P_nat[1] #element wise product
|
||||||
self.ifft_operator()
|
self.ifft_operator()
|
||||||
|
#field before MS2
|
||||||
self.intermediate_fields[1] = np.fft.fftshift(self.ifft_operator.output_array.copy())
|
self.intermediate_fields[1] = np.fft.fftshift(self.ifft_operator.output_array.copy())
|
||||||
#print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(self.intermediate_fields[0])**2))
|
#print("Field intensity after first phase mask and propagation: %.4e" % np.sum(np.abs(self.intermediate_fields[0])**2))
|
||||||
|
|
||||||
@@ -560,6 +587,7 @@ class Simulation:
|
|||||||
self.fft_operator()
|
self.fft_operator()
|
||||||
self.ifft_input_array[:,:] = self.fft_operator.output_array * self.P_nat[2] #element wise product
|
self.ifft_input_array[:,:] = self.fft_operator.output_array * self.P_nat[2] #element wise product
|
||||||
self.ifft_operator()
|
self.ifft_operator()
|
||||||
|
#output field
|
||||||
self.intermediate_fields[2] = np.fft.fftshift(self.ifft_operator.output_array.copy())
|
self.intermediate_fields[2] = np.fft.fftshift(self.ifft_operator.output_array.copy())
|
||||||
|
|
||||||
self.output_field = self.intermediate_fields[-1].flatten()
|
self.output_field = self.intermediate_fields[-1].flatten()
|
||||||
@@ -609,7 +637,9 @@ class Simulation:
|
|||||||
return grad_C_phi_1, grad_C_phi_2
|
return grad_C_phi_1, grad_C_phi_2
|
||||||
|
|
||||||
def _value_and_grad(self, target):
|
def _value_and_grad(self, target):
|
||||||
|
'''
|
||||||
|
|
||||||
|
'''
|
||||||
source = target.get_associated_source()
|
source = target.get_associated_source()
|
||||||
wavelength = source.get_wavelength()
|
wavelength = source.get_wavelength()
|
||||||
|
|
||||||
@@ -640,7 +670,26 @@ class Simulation:
|
|||||||
|
|
||||||
return w_full.flatten()
|
return w_full.flatten()
|
||||||
|
|
||||||
def _cost_fun(self, x, grad):
|
def _epigraph_constraint(self, result, epigraph_and_weights, gradient)->None:
|
||||||
|
'''
|
||||||
|
Constraint function for the epigraph formulation.
|
||||||
|
Check nlopt documentation about Vector-valued constraints for additional details.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: 1D array, modified in place, with the results of this constraint
|
||||||
|
evaluation. Dimensionality should match the number of constraints.
|
||||||
|
|
||||||
|
epigraph_and_weights: 1D array of epigraph (first element) and
|
||||||
|
design weights (remaining elements)
|
||||||
|
|
||||||
|
gradient: a 2D array of gradients, modified in place, of the constraint function with respect to the design weights.
|
||||||
|
Shape should be mxn, where m is the number of constraints and n is the number of design weights.
|
||||||
|
|
||||||
|
'''
|
||||||
|
print("evaluating epigraph constraint")
|
||||||
|
epigraph = epigraph_and_weights[0]
|
||||||
|
x = epigraph_and_weights[1:]
|
||||||
|
|
||||||
if self.space_grid.xy_symmetry:
|
if self.space_grid.xy_symmetry:
|
||||||
ms1_first_quadrant = x[:self.space_grid.get_dof()//2].reshape(self.space_grid.get_Nx()//2+1, self.space_grid.get_Ny()//2+1)
|
ms1_first_quadrant = x[:self.space_grid.get_dof()//2].reshape(self.space_grid.get_Nx()//2+1, self.space_grid.get_Ny()//2+1)
|
||||||
ms2_first_quadrant = x[self.space_grid.get_dof()//2:].reshape(self.space_grid.get_Nx()//2+1, self.space_grid.get_Ny()//2+1)
|
ms2_first_quadrant = x[self.space_grid.get_dof()//2:].reshape(self.space_grid.get_Nx()//2+1, self.space_grid.get_Ny()//2+1)
|
||||||
@@ -654,13 +703,100 @@ 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:
|
if self.use_filtering:
|
||||||
|
phase_mask_1 = self.phase_mask[:self.space_grid.get_S()]
|
||||||
|
phase_mask_2 = self.phase_mask[self.space_grid.get_S():]
|
||||||
filtered_phase_mask_1 = self._phase_filter(phase_mask_1)
|
filtered_phase_mask_1 = self._phase_filter(phase_mask_1)
|
||||||
filtered_phase_mask_2 = self._phase_filter(phase_mask_2)
|
filtered_phase_mask_2 = self._phase_filter(phase_mask_2)
|
||||||
|
|
||||||
self.filtered_phase_mask[:] = npa.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
|
||||||
|
|
||||||
|
#after the phase masks are set, we need to compute value and gradients for each target
|
||||||
|
grads_matrix = np.zeros((len(self.targets), self.space_grid.get_dof()))
|
||||||
|
results_array = np.zeros(len(self.targets))
|
||||||
|
|
||||||
|
for idx, target in enumerate(self.targets):
|
||||||
|
|
||||||
|
C, full_grad_1, full_grad_2 = self._value_and_grad(target)
|
||||||
|
#minus sign required because the constraint is in the form of f - t < 0 and we minimize the epigraph t
|
||||||
|
results_array[idx] = - C
|
||||||
|
|
||||||
|
if self.use_filtering:
|
||||||
|
jac_grad_1 = tensor_jacobian_product(self._phase_filter)(phase_mask_1, full_grad_1)
|
||||||
|
print("jac 1 done")
|
||||||
|
jac_grad_2 = tensor_jacobian_product(self._phase_filter)(phase_mask_2, full_grad_2)
|
||||||
|
print("jac 2 done")
|
||||||
|
else:
|
||||||
|
jac_grad_1 = full_grad_1
|
||||||
|
jac_grad_2 = full_grad_2
|
||||||
|
|
||||||
|
if self.space_grid.xy_symmetry:
|
||||||
|
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)
|
||||||
|
|
||||||
|
grads_matrix[idx, :] = jac_grad
|
||||||
|
|
||||||
|
result[:] = results_array - epigraph
|
||||||
|
|
||||||
|
if gradient.size > 0:
|
||||||
|
gradient[:,0] = -1
|
||||||
|
gradient[:,1:] = - grads_matrix * 2 * np.pi
|
||||||
|
|
||||||
|
|
||||||
|
def _obj_fun(self, epigraph_and_weights, grad):
|
||||||
|
''' Objective function for the epigraph formulation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
epigraph_and_weights: 1D array of epigraph (first element) and
|
||||||
|
design weights (remaining elements)
|
||||||
|
grad: gradient of the objective function with respect to the design weights, modified in place
|
||||||
|
Returns:
|
||||||
|
The scalar epigraph variable (the objective function)
|
||||||
|
'''
|
||||||
|
|
||||||
|
epigraph = epigraph_and_weights[0]
|
||||||
|
|
||||||
|
if grad.size > 0:
|
||||||
|
grad[0] = 1
|
||||||
|
grad[1:] = 0
|
||||||
|
|
||||||
|
self.iter_num[0] = self.iter_num[0] + 1
|
||||||
|
self.opt_history.append(epigraph)
|
||||||
|
|
||||||
|
self._plot_history()
|
||||||
|
self.iter_num[:] += [self.iter_num[0]+1]
|
||||||
|
|
||||||
|
return epigraph
|
||||||
|
|
||||||
|
def _cost_fun(self, x, grad):
|
||||||
|
self.iter_num[0] = self.iter_num[0] + 1
|
||||||
|
if self.space_grid.xy_symmetry:
|
||||||
|
ms1_first_quadrant = x[:self.space_grid.get_dof()//2].reshape(self.space_grid.get_Nx()//2+1, self.space_grid.get_Ny()//2+1)
|
||||||
|
ms2_first_quadrant = x[self.space_grid.get_dof()//2:].reshape(self.space_grid.get_Nx()//2+1, self.space_grid.get_Ny()//2+1)
|
||||||
|
ms1 = self._expand_symmetrize(ms1_first_quadrant)
|
||||||
|
ms2 = self._expand_symmetrize(ms2_first_quadrant)
|
||||||
|
else:
|
||||||
|
ms1 = x[:self.space_grid.get_dof()//2]
|
||||||
|
ms2 = x[self.space_grid.get_dof()//2:]
|
||||||
|
|
||||||
|
all_ms = np.concatenate((ms1, ms2))
|
||||||
|
|
||||||
|
self.w_norm[:] = all_ms
|
||||||
|
self.phase_mask[:] = self._phase_given_w(self.w_norm)
|
||||||
|
|
||||||
|
if self.use_filtering:
|
||||||
|
phase_mask_1 = self.phase_mask[:self.space_grid.get_S()]
|
||||||
|
phase_mask_2 = self.phase_mask[self.space_grid.get_S():]
|
||||||
|
filtered_phase_mask_1 = self._phase_filter(phase_mask_1)
|
||||||
|
filtered_phase_mask_2 = self._phase_filter(phase_mask_2)
|
||||||
|
|
||||||
|
self.filtered_phase_mask[:] = np.concatenate((filtered_phase_mask_1, filtered_phase_mask_2))
|
||||||
else:
|
else:
|
||||||
self.filtered_phase_mask[:] = self.phase_mask
|
self.filtered_phase_mask[:] = self.phase_mask
|
||||||
|
|
||||||
@@ -674,8 +810,12 @@ class Simulation:
|
|||||||
|
|
||||||
C_all_wavelengths.append(C)
|
C_all_wavelengths.append(C)
|
||||||
|
|
||||||
jac_grad_1 = tensor_jacobian_product(self._phase_filter)(phase_mask_1, full_grad_1)
|
if self.use_filtering:
|
||||||
jac_grad_2 = tensor_jacobian_product(self._phase_filter)(phase_mask_2, full_grad_2)
|
jac_grad_1 = tensor_jacobian_product(self._phase_filter)(phase_mask_1, full_grad_1)
|
||||||
|
jac_grad_2 = tensor_jacobian_product(self._phase_filter)(phase_mask_2, full_grad_2)
|
||||||
|
else:
|
||||||
|
jac_grad_1 = full_grad_1
|
||||||
|
jac_grad_2 = full_grad_2
|
||||||
|
|
||||||
if self.space_grid.xy_symmetry:
|
if self.space_grid.xy_symmetry:
|
||||||
jac_grad_1_symm = self._reduce_symmetrized_gradient(jac_grad_1)
|
jac_grad_1_symm = self._reduce_symmetrized_gradient(jac_grad_1)
|
||||||
@@ -691,29 +831,63 @@ class Simulation:
|
|||||||
|
|
||||||
#the new cost function considers the average overlap across all wavelengths.
|
#the new cost function considers the average overlap across all wavelengths.
|
||||||
self.opt_history.append(np.mean(C_all_wavelengths))
|
self.opt_history.append(np.mean(C_all_wavelengths))
|
||||||
self.iter_num[0] += 1
|
self._plot_history()
|
||||||
|
return np.mean(C_all_wavelengths)
|
||||||
|
|
||||||
|
def _plot_history(self):
|
||||||
|
'''
|
||||||
|
Utility function to plot the optimization history and the cost function
|
||||||
|
'''
|
||||||
print("Iteration:", self.iter_num[0])
|
print("Iteration:", self.iter_num[0])
|
||||||
if self.iter_num[0] % 5 == 0 and self.results_while_solving:
|
if self.iter_num[0] % 5 == 0 and self.results_while_solving:
|
||||||
fig, ax = plt.subplots()
|
fig, ax = plt.subplots()
|
||||||
ax.plot(self.opt_history)
|
ax.plot(self.opt_history)
|
||||||
ax.set_xlabel('Iteration')
|
ax.set_xlabel('Iteration')
|
||||||
ax.set_ylabel('Cost Function Value')
|
ax.set_ylabel('Epigraph value')
|
||||||
ax.set_title('Optimization History')
|
ax.set_title('Optimization History')
|
||||||
fig.tight_layout()
|
fig.tight_layout()
|
||||||
fig.savefig(f"{self.results_folder}/optimization_history.pdf")
|
fig.savefig(f"{self.results_folder}/optimization_history.pdf")
|
||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
|
|
||||||
#make a plot of the cost function values across
|
def _solver_init(self):
|
||||||
# all wavelengths for this iteration
|
'''
|
||||||
fig, ax = plt.subplots()
|
This method sets up the optimizer based on the chosen strategy.
|
||||||
ax.plot(self.get_wvl_all_targets(unit_measure=self.space_grid.m_to_nm), C_all_wavelengths, marker='o')
|
|
||||||
ax.set_xlabel(f'Wavelength [nm]')
|
|
||||||
ax.set_ylabel('Cost function value')
|
|
||||||
fig.tight_layout()
|
|
||||||
fig.savefig(f"{self.results_folder}/cost_function_across_wavelengths.pdf")
|
|
||||||
plt.close(fig)
|
|
||||||
|
|
||||||
return np.mean(C_all_wavelengths)
|
'''
|
||||||
|
if self.strategy == 'mean':
|
||||||
|
self.solver = nlopt.opt(nlopt.LD_CCSAQ, self.space_grid.get_dof())
|
||||||
|
|
||||||
|
self.solver.set_lower_bounds(self.chosen_mask_lower_bounds.flatten())
|
||||||
|
self.solver.set_upper_bounds(self.chosen_mask_upper_bounds.flatten())
|
||||||
|
|
||||||
|
self.solver.set_max_objective(self._cost_fun)
|
||||||
|
self.solver.set_maxeval(self.opt_max_eval)
|
||||||
|
|
||||||
|
elif self.strategy == 'epigraph':
|
||||||
|
self.solver = nlopt.opt(nlopt.LD_CCSAQ, self.space_grid.get_dof() + 1)
|
||||||
|
epigraph_and_weights_lower_bounds = np.insert(self.chosen_mask_lower_bounds.flatten(), 0, -np.inf)
|
||||||
|
epigraph_and_weights_upper_bounds = np.insert(self.chosen_mask_upper_bounds.flatten(), 0, np.inf)
|
||||||
|
|
||||||
|
self.solver.set_lower_bounds(epigraph_and_weights_lower_bounds)
|
||||||
|
self.solver.set_upper_bounds(epigraph_and_weights_upper_bounds)
|
||||||
|
|
||||||
|
self.solver.set_min_objective(self._obj_fun)
|
||||||
|
epigraph_tolerance = np.array([1e-4] * self.get_num_targets())
|
||||||
|
self.solver.add_inequality_mconstraint(
|
||||||
|
lambda result_, epigraph_and_weights_, grad_: self._epigraph_constraint(
|
||||||
|
result_,
|
||||||
|
epigraph_and_weights_,
|
||||||
|
grad_
|
||||||
|
),
|
||||||
|
epigraph_tolerance
|
||||||
|
)
|
||||||
|
self.solver.set_maxeval(self.opt_max_eval)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown optimization strategy: {self.strategy}")
|
||||||
|
|
||||||
|
self.solver.set_param("dual_ftol_rel", 1e-5)
|
||||||
|
self.solver.set_param("inner_maxeval", 30)
|
||||||
|
self.solver.set_param("verbosity", 0)
|
||||||
|
|
||||||
def _export_results(self):
|
def _export_results(self):
|
||||||
'''
|
'''
|
||||||
@@ -727,18 +901,22 @@ 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=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=1e6*self.get_ms_max_avail_diameter()/2, plot_zoom_y=1e6*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")
|
make_2Dplot_of(output_field[-1], self.space_grid.get_X(), self.space_grid.get_Y(), plot_zoom_x=1e6*self.get_ms_max_avail_diameter()/2, plot_zoom_y=1e6*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()))
|
||||||
filtered_phase_mask_1 = self._phase_filter(phase_mask_1)
|
if self.use_filtering:
|
||||||
filtered_phase_mask_2 = self._phase_filter(phase_mask_2)
|
filtered_phase_mask_1 = self._phase_filter(phase_mask_1)
|
||||||
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")
|
filtered_phase_mask_2 = self._phase_filter(phase_mask_2)
|
||||||
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")
|
else:
|
||||||
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")
|
filtered_phase_mask_1 = phase_mask_1
|
||||||
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")
|
filtered_phase_mask_2 = phase_mask_2
|
||||||
|
make_2Dplot_of(phase_mask_1, self.space_grid.get_X(), self.space_grid.get_Y(), plot_zoom_x=1e6*self.get_ms_max_avail_diameter()/2, plot_zoom_y=1e6*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=1e6*self.get_ms_max_avail_diameter()/2, plot_zoom_y=1e6*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=1e6*self.get_ms_max_avail_diameter()/2, plot_zoom_y=1e6*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=1e6*self.get_ms_max_avail_diameter()/2, plot_zoom_y=1e6*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)
|
||||||
@@ -779,26 +957,59 @@ class Simulation:
|
|||||||
print("Starting optimization...")
|
print("Starting optimization...")
|
||||||
self._initialize_phase_masks()
|
self._initialize_phase_masks()
|
||||||
#initialize nlopt solver
|
#initialize nlopt solver
|
||||||
solver = nlopt.opt(nlopt.LD_CCSAQ, self.space_grid.get_dof())
|
|
||||||
solver.set_lower_bounds(self.chosen_mask_lower_bounds.flatten())
|
|
||||||
solver.set_upper_bounds(self.chosen_mask_upper_bounds.flatten())
|
|
||||||
solver.set_max_objective(self._cost_fun)
|
|
||||||
solver.set_maxeval(self.opt_max_eval)
|
|
||||||
solver.set_param("dual_ftol_rel", 1e-7)
|
|
||||||
solver.set_param("verbosity",0)
|
|
||||||
|
|
||||||
if self.space_grid.xy_symmetry:
|
if self.space_grid.xy_symmetry:
|
||||||
self.w_norm_symm[:] = solver.optimize(self.w_norm_symm)
|
if self.strategy == 'epigraph':
|
||||||
|
epigraph_w_norm_symm = self.solver.optimize(np.insert(self.w_norm_symm, 0, 0))
|
||||||
|
self.w_norm_symm[:] = epigraph_w_norm_symm[1:]
|
||||||
|
elif self.strategy == 'mean':
|
||||||
|
self.w_norm_symm[:] = self.solver.optimize(self.w_norm_symm)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown strategy: {self.strategy}")
|
||||||
self.is_optimized_flag = True
|
self.is_optimized_flag = True
|
||||||
self.best_opt_val = solver.last_optimum_value()
|
self.best_opt_val = self.solver.last_optimum_value()
|
||||||
self.w_norm[:self.space_grid.get_S()] = self._expand_symmetrize(self.w_norm_symm[:self.space_grid.get_dof()//2])
|
self.w_norm[:self.space_grid.get_S()] = self._expand_symmetrize(self.w_norm_symm[:self.space_grid.get_dof()//2])
|
||||||
self.w_norm[self.space_grid.get_S():] = self._expand_symmetrize(self.w_norm_symm[self.space_grid.get_dof()//2:])
|
self.w_norm[self.space_grid.get_S():] = self._expand_symmetrize(self.w_norm_symm[self.space_grid.get_dof()//2:])
|
||||||
else:
|
self.phase_mask[:] = self._phase_given_w(self.w_norm)
|
||||||
self.w_norm[:] = solver.optimize(self.w_norm)
|
|
||||||
self.is_optimized_flag = True
|
|
||||||
self.best_opt_val = solver.last_optimum_value()
|
|
||||||
|
|
||||||
self._export_results()
|
if self.use_filtering:
|
||||||
|
phase_mask_1 = self.phase_mask[:self.space_grid.get_S()]
|
||||||
|
phase_mask_2 = self.phase_mask[self.space_grid.get_S():]
|
||||||
|
filtered_phase_mask_1 = self._phase_filter(phase_mask_1)
|
||||||
|
filtered_phase_mask_2 = self._phase_filter(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
|
||||||
|
|
||||||
|
self.is_optimization_set = True
|
||||||
|
self.best_opt_val = self.solver.last_optimum_value()
|
||||||
|
else:
|
||||||
|
if self.strategy == 'epigraph':
|
||||||
|
self.epigraph_w_norm = np.insert(self.w_norm, 0, 0)
|
||||||
|
self.epigraph_w_norm[:] = self.solver.optimize(self.epigraph_w_norm)
|
||||||
|
self.w_norm[:] = self.epigraph_w_norm[1:]
|
||||||
|
elif self.strategy == 'mean':
|
||||||
|
self.w_norm[:] = self.solver.optimize(self.w_norm)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown strategy: {self.strategy}")
|
||||||
|
self.phase_mask[:] = self._phase_given_w(self.w_norm)
|
||||||
|
|
||||||
|
if self.use_filtering:
|
||||||
|
phase_mask_1 = self.phase_mask[:self.space_grid.get_S()]
|
||||||
|
phase_mask_2 = self.phase_mask[self.space_grid.get_S():]
|
||||||
|
filtered_phase_mask_1 = self._phase_filter(phase_mask_1)
|
||||||
|
filtered_phase_mask_2 = self._phase_filter(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
|
||||||
|
|
||||||
|
self.is_optimized_flag = True
|
||||||
|
self.best_opt_val = self.solver.last_optimum_value()
|
||||||
|
|
||||||
|
if self.save_results:
|
||||||
|
self._export_results()
|
||||||
|
|
||||||
|
return self.best_opt_val
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("Optimization settings have not been specified.")
|
raise RuntimeError("Optimization settings have not been specified.")
|
||||||
|
|
||||||
+14
-12
@@ -6,20 +6,20 @@ if not os.path.exists("results"):
|
|||||||
|
|
||||||
#Space grid definition
|
#Space grid definition
|
||||||
N = 1025
|
N = 1025
|
||||||
unit_cell_pitch = 700e-9
|
unit_cell_pitch = 0.700 #i.e., 700 [nm]
|
||||||
|
|
||||||
n0 = 1.45
|
n0 = 1.45
|
||||||
n1 = 1.0
|
n1 = 1.0
|
||||||
n2 = 1.45
|
n2 = 1.45
|
||||||
mat_refr_index = np.array([n0, n1, n2])
|
mat_refr_index = np.array([n0, n1, n2])
|
||||||
|
|
||||||
d0 = 476e-6 #propagation distance: source - MS1 [m]
|
d0 = 476 #propagation distance: source - MS1 [um]
|
||||||
d1 = 510e-6 #propagation distance: MS1 - MS2 [m]
|
d1 = 510 #propagation distance: MS1 - MS2 [um]
|
||||||
d2 = d0 #propagation distance: MS2 - target [m]
|
d2 = d0 #propagation distance: MS2 - target [um]
|
||||||
d = np.array([d0, d1, d2])
|
d = np.array([d0, d1, d2])
|
||||||
|
|
||||||
ms_max_available_diameter = 122e-6 #[um]
|
ms_max_available_diameter = 122 #[um]
|
||||||
output_fiber_core_diameter = 19e-6 #[um]
|
output_fiber_core_diameter = 19 #[um]
|
||||||
opt_max_eval = 140
|
opt_max_eval = 140
|
||||||
|
|
||||||
space_grid = pid.SpaceGrid(
|
space_grid = pid.SpaceGrid(
|
||||||
@@ -29,7 +29,7 @@ space_grid = pid.SpaceGrid(
|
|||||||
materials_refr_index=mat_refr_index,
|
materials_refr_index=mat_refr_index,
|
||||||
)
|
)
|
||||||
|
|
||||||
wavelength = 1.55e-6
|
wavelength = 1.55
|
||||||
|
|
||||||
#define the sources
|
#define the sources
|
||||||
source1 = pid.Source(input_field='fiber_modes/LP11_field_distribution_lda_1550_N_1025.npy',
|
source1 = pid.Source(input_field='fiber_modes/LP11_field_distribution_lda_1550_N_1025.npy',
|
||||||
@@ -43,13 +43,13 @@ source2 = pid.Source(input_field='fiber_modes/LP21_field_distribution_lda_1550_N
|
|||||||
|
|
||||||
|
|
||||||
target = pid.Target(input_field='fiber_modes/LP01_field_distribution_lda_1550_N_1025.npy',
|
target = pid.Target(input_field='fiber_modes/LP01_field_distribution_lda_1550_N_1025.npy',
|
||||||
center=(20e-6,0),
|
center=np.array([20,0]),
|
||||||
associated_source=source1,
|
associated_source=source1,
|
||||||
wavelength=wavelength,
|
wavelength=wavelength,
|
||||||
space_grid=space_grid,
|
space_grid=space_grid,
|
||||||
field_norm=True)
|
field_norm=True)
|
||||||
target2 = pid.Target(input_field='fiber_modes/LP01_field_distribution_lda_1550_N_1025.npy',
|
target2 = pid.Target(input_field='fiber_modes/LP01_field_distribution_lda_1550_N_1025.npy',
|
||||||
center=(-20e-6,0),
|
center=np.array([-20,0]),
|
||||||
associated_source=source2,
|
associated_source=source2,
|
||||||
wavelength=wavelength,
|
wavelength=wavelength,
|
||||||
space_grid=space_grid,
|
space_grid=space_grid,
|
||||||
@@ -69,15 +69,17 @@ sim = pid.Simulation(
|
|||||||
n_cpu=1
|
n_cpu=1
|
||||||
)
|
)
|
||||||
|
|
||||||
opt_param_min = 0
|
opt_param_min = 0.0
|
||||||
opt_param_max = 1
|
opt_param_max = 1.0
|
||||||
|
|
||||||
sim.set_optimization(opt_param_min=opt_param_min,
|
sim.set_optimization(opt_param_min=opt_param_min,
|
||||||
opt_param_max=opt_param_max,
|
opt_param_max=opt_param_max,
|
||||||
ms_max_avail_diameter=ms_max_available_diameter,
|
ms_max_avail_diameter=ms_max_available_diameter,
|
||||||
phase_mask_init_size=output_fiber_core_diameter,
|
phase_mask_init_size=output_fiber_core_diameter,
|
||||||
opt_max_eval=opt_max_eval,
|
opt_max_eval=opt_max_eval,
|
||||||
conic_filter_radius=1.5e-6)
|
strategy = 'mean',
|
||||||
|
conic_filter_radius = 1.0, #i.e. 700 [nm]
|
||||||
|
)
|
||||||
|
|
||||||
sim.run()
|
sim.run()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user