Program Sequence#
In this section, we will provide a detailed explanation of the functions in Meent and discuss the simulation program sequence with examples. Specifically, we will demonstrate an example code for simulating and optimizing the optical response of a silicon rectangular pillar in a periodic structure.
Initialization#
A simple way to use Meent is using call_mee() function which returns an instance of Python class that includes
all the functionalities of Meent. Simulation conditions can be set by passing parameters as arguements (args) or
keyword arguements (kwargs) in this function. It is also possible to change conditions after calling instance
by directly assigning desired value to the property of the instance.
Methods to set simulation conditions
# method 1: thickness setting in instance call
mee = meent.call_mee(backend=backend, thickness=thickness, ...)
# method 2: direct assignment
mee = meent.call_mee(backend=backend, ...)
mee.thickness = thickness
Here are the descriptions of the input parameters in Meent class:
backendintegersupports three backends: NumPy, JAX, and PyTorch.
0: NumPy (RCWA only; AD is not supported).
1: JAX.
2: PyTorch.
grating_typeintegerThis parameter defines the simulation space.
0: 1D grating without conical incidence \((\phi = 0)\).
1: 1D grating with conical incidence.
2: 2D grating.
pol: integer or floatThis parameter controls the linear polarization state of the incident wave by this definition: \(\psi = \pi / 2 * (1 - {\textit{pol}})\). It can take values between 0 and 1. 0 represents fully transverse electric (TE) polarization, and 1 represents fully transverse magnetic (TM) polarization. Support for other polarization states such as the circular polarization state which involves the phase difference between TE and TM polarization will be added in the future updates.
n_IfloatThe refractive index of the superstrate.
n_IIfloatThe refractive index of the substrate.
thetafloatThe angle of the incidence in radians.
phifloatThe angle of rotation (or azimuth angle) in radians.
wavelengthfloatThe wavelength of the incident light in vacuum. Future versions may support complex type wavelength.
fourier_orderinteger or list of integersFourier truncation order (FTO). This represents the number of Fourier harmonics in use. If
fourier_order= \(N\), this is for 1D grating and Meent utilizes \((2N+1)\) harmonics spanning from \(-N\) to \(N\):\(-N, -(N-1), ..., N\). For 2D gratings, it takes a sequence \([M,N]\) as an input, where \(M\) and \(N\) become FTO in \(X\) and \(Y\) directions, respectively. Note that 1D grating can also be simulated in 2D grating system by setting \(N\) as \(0\).periodlist of floatsThe period of a unit cell. For 1D grating, it is a sequence with one element which is a period in X-direction. For 2D gratings, it takes a sequence [period in \(X\), period in \(Y\)] as an input.
type_complexintegerThe datatype used in the simulation.
0: complex128 (64 bit).
1: complex64 (32 bit).
deviceintegerThe selection of the device for the calculations: currently CPU and GPU are supported. At the time of writing this paper, the eigendecomposition, which is the most expensive step as \(\mathcal{O}(M^3N^3)\) where \(M\text{ and } N\) are FTO, is available only on CPU. This means GPU may not as powerful as we expect as in deep learning regime.
0: CPU.
1: GPU.
2: TPU. As of now, TPU is not supported since data transfer between TPU and CPU for eigendecomposition is not enabled.
fft_typeintegerThis variable selects the type of Fourier series implementation. 0 and 1 are options for raster modeling and 2 is for vector modeling. 0 uses discrete Fourier series (DFS) while 1 and 2 use continuous Fourier series (CFS). Note that the name `fft_type’ may change since it is not correct expression.
- 0: DFS for the raster modeling (pixel-based geometry).
fft_typesupportsimprove_dftoption, which is True by default, that can prevent aliasing by increasing sampling frequency, and drives the result to approach to the result of CFS.
- 1: CFS for the raster modeling (pixel-based geometry).
This doesn’t support backpropagation. Use this option for debugging or in RCWA-only situation.
2: CFS for the vector modeling (object-based geometry).
thicknesslist of floatsThe sequence of the thickness of each layer from top to bottom.
ucellarray of {floats, complex numbers}, shape is (i, j, k)The input for the raster modeling. It takes a 3D array in (\(Z\), \(Y\), \(X\)) order, where \(Z\) represents the direction of the layer stacking. In case of 1D grating, j is 1 (e.g., shape = (3,1,10) for a stack composed of 3 layers that are 1D grating).
Geometry Modeling#
Meent provides two types of geometry modeling methods: raster and vector.
Raster Modeling#
Figure 1: Raster-type structure examples. (a) 2 layers in 1D and (b) 1 layer in 2D grating.
# (a): 1D grating with 2 layers
ucell = np.array(
[
[[1, 1, 1, 3.48, 3.48, 3.48, 3.48, 1, 1, 1]],
[[1, 3.48, 3.48, 1, 1, 1, 1, 3.48, 3.48, 1]],
]) # array shape: (2, 1, 10)
# (b): 2D grating with 1 layers
ucell = np.array(
[[
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 3.48, 3.48, 3.48, 3.48, 1, 1, 1],
[1, 1, 1, 3.48, 3.48, 3.48, 3.48, 1, 1, 1],
[1, 1, 1, 3.48, 3.48, 3.48, 3.48, 1, 1, 1],
[1, 1, 1, 3.48, 3.48, 3.48, 3.48, 1, 1, 1],
[1, 1, 1, 3.48, 3.48, 3.48, 3.48, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
]]) # array shape: (1, 8, 10)
mee = meent.call_mee(backend=backend, ucell=ucell)
We have 2 example structures of raster modeling as shown in Figure 1 and Code 1. Figure 1a is a stack of 2 layers which has 1D grating. Note that 1D grating unit cell can be defined by setting the length of the second axis to 1 as (a) in Code 1. Figure 1b is a stack of single 2D grating layer.
Vector Modeling#
Figure 2: Rotated rectangles with approximation. Light blue is the ideal one and light red is approximated one.
thickness = [300.]
length_x = 100
length_y = 300
center = [300, 500]
n_index_1 = 3.48
n_index_2 = 1
base_n_index_of_layer = n_index_2
angle = 35 * torch.pi / 180
n_split = [5, 5] # degree of approximation
length_x = torch.tensor(length_x, dtype=torch.float64, requires_grad=True)
length_y = torch.tensor(length_y, dtype=torch.float64, requires_grad=True)
thickness = torch.tensor(thickness, requires_grad=True)
angle = torch.tensor(angle, requires_grad=True)
obj_list = mee.rectangle_rotate(*center, length_x, length_y, *n_split, n_index_1, angle)
layer_info_list = [[base_n_index_of_layer, obj_list]]
mee.draw(layer_info_list)
Figure 2 shows rotated rectangles drawn on XY plane. Meent decomposes the geometrical figures into the collection
of sub-rectangles which of each side lies on the direction of either \(\hat x\) or \(\hat y\).
Then CFS with the sinc function is used to find the Fourier coefficients. The degree of approximation can be
determined by n_split option in Code 2.
To add primitives to the simulation space, users can utilize `rectangle()’ or `rectangle_rotation()’ functions which allows the insertion of desired geometry. The `draw()’ function is then employed to create the final structure, taking into account any potential overlaps between the geometries. Code ref{code:vector} is the example creating a layer that has rotated rectangle.
Figure 3: The overlap of 2 rectangles in vector modeling. The hierarchy is determined by the index of the objects in the list.
red_rect = mee.rectangle_rotate(*[400, 500], 400, 600, 20, 20, 3.5, 0)
blue_rect = mee.rectangle_rotate(*[600, 500], 100, 600, 40, 40, 10, -20)
layer_info_list = [[2.4, red_rect + blue_rect]] # red bottom, blue top
layer_info_list = [[2.4, blue_rect + red_rect]] # blue bottom, red top
mee.draw(layer_info_list)
de_ri, de_ti = mee.conv_solve()
Code 3 and Figure 3 show how Meent can handle the overlap of the shapes.
Figure 3a and 3b have the same set of rectangles (red and blue) but they are placed
in different order and this can be controlled by the argument layer_info_list in Code 3.
It is the list that contains the base refractive index of the layer and the primitive shapes to be placed on the layer.
In case of Figure 3a, red rectangle comes first in the list and blue does for Figure 3b.
Electromagnetic Simulation#
mee = call_mee(backend, ...)
# generates convolution matrix and solves Maxwell's equation.
de_ri, de_ti = mee.conv_solve()
# generates convolution matrix, solves Maxwell's equation and
# reconstructs field distribution.
de_ri, de_ti, field_cell = mee.conv_solve_field()
Electromagnetic simulation (EM simulation) in Meent can be divided into 3 main subcategories:
convolution matrix generation, Maxwell’s equations computation and field calculation.
The method conv_solve() does both convolution matrix generation and Maxwell’s equations
computation sequentially. conv_solve_field() method does the same and additionally calculates
the field distribution of the structure. Code ref{code:em_method} is the example showing
how to use those; conv_solve() method returns the reflected and transmitted diffraction
efficiencies and conv_solve_field() does both and field distribution.
Convolution Matrix Generation#
Figure 4: Material property in Fourier space. (a) Coefficients matrix of Fourier analysis and (b) convolution matrix generated by re-arranging (circulant matrix) Fourier coefficients.
conv_solve().# def conv_solve(self, **kwargs):
[setattr(self, k, v) for k, v in kwargs.items()]
# needed for optimization
if self.fft_type == 0: # raster with DFS
E_conv_all, o_E_conv_all = to_conv_mat_raster_discrete(self.ucell, self.fourier_order[0], self.fourier_order[1], device=self.device, type_complex=self.type_complex, improve_dft=self.improve_dft)
elif self.fft_type == 1: # raster with CFS
E_conv_all, o_E_conv_all = to_conv_mat_raster_continuous(self.ucell, self.fourier_order[0], self.fourier_order[1], device=self.device, type_complex=self.type_complex)
elif self.fft_type == 2: # vector with CFS
E_conv_all, o_E_conv_all = to_conv_mat_vector(self.ucell_info_list, self.fourier_order[0], self.fourier_order[1], type_complex=self.type_complex)
else:
raise ValueError
de_ri, de_ti, layer_info_list, T1, kx_vector = self._solve(self.wavelength, E_conv_all, o_E_conv_all)
self.layer_info_list = layer_info_list
self.T1 = T1
self.kx_vector = kx_vector
return de_ri, de_ti
The functions for convolution matrix generation are located in convolution_matrix.py file for each backend.
This part transforms the structure from the real space to the Fourier space and returns a convolution matrix
(also called Toeplitz matrix) of the Fourier coefficients to apply convolution operation with the E and H fields.
Figure 4 shows the Fourier coefficients matrix and convolution matrix
made from the coefficient matrix.
Code 5 is the definition of conv_solve() method and shows how the convolution matrix generation
is integrated inside. As shown in the code, Meent offers 3 different methods to get convolution matrix since
each method has different input type and implementation. This can be chosen by the argument fft_type: 0 is for
raster modeling with DFS, 1 for raster with CFS and 2 for vector with CFS.
Maxwell’s Equations Computation#
solve().# def solve(self, wavelength, e_conv_all, o_e_conv_all):
de_ri, de_ti, layer_info_list, T1, kx_vector = self._solve(wavelength, e_conv_all, o_e_conv_all)
# internal info. for the field calculation
self.layer_info_list = layer_info_list
self.T1 = T1
self.kx_vector = kx_vector
return de_ri, de_ti
wavelengthfloatThe wavelength of the incident light in vacuum.
e_conv_allarray of {float or complex}A stack of convolution matrices of the permittivity array; this is \(\left[\!\!\left[\varepsilon_{r,g}^{}\right]\!\!\right]\) in Background theory. The order of the axes is the same as of ucell (\(Z, Y, X\)).
o_e_conv_allarray of {float or complex}A stack of convolution matrices of the one-over-permittivity array; this is \(\left[\!\!\left[\varepsilon_{r,g}^{-1}\right]\!\!\right]\) in Background theory. The order of the axes is the same as of ucell (\(Z, Y, X\)).
After generating the convolution matrix, Meent solves Maxwell’s equations
and returns diffraction efficiencies with the method solve(). As in the Code 6,
it is a wrapper of _solve() method that actually does the calculations and returns
the diffraction efficiencies with other information that is necessary for the field calculation.
The diffraction efficiencies are 1D array for 1D and 1D-conical grating and 2D for 2D grating.
Field Calculation#
Figure 5: Field distribution on XY plane from 2D grating structure. (a)-(c): absolute value of the electric field in each direction, (d)-(f): absolute value of the magnetic field in each direction.
calculate_field().# field_cell = mee.calculate_field(res_x=100, res_y=100, res_z=100)
res_xintegerThe field resolution in X direction (number of split which the period of x is divided by).
res_yintegerThe field resolution in Y direction (number of split which the period of y is divided by).
res_zintegerThe field resolution in Z direction (number of split in thickness of each layer).
field_algointegerThe level of vectorization for the field calculation. Default is 2 which is fully vectorized for fast calculation while 1 is half-vectorized and 0 is none. Option 0 and 1 are remained for debugging or future development (such as parallelization).
0: Non-vectorized
1: Semi-vectorized: in X and Y direction
2: Vectorized: in X, Y and Z direction
calculate_field().# # 1D TE and TM case
field_cell = torch.zeros((res_z * len(layer_info_list), res_y, res_x, 3), dtype=type_complex)
# 1D conincal and 2D case
field_cell = torch.zeros((res_z * len(layer_info_list), res_y, res_x, 6), dtype=type_complex)
The calculate_field() method in Code 7 calculates the field distribution inside the
structure. Note that the solve() method must be preceded.
This function returns 4 dimensional array that the length of the last axis varies depending on the grating type
as shown in Code 8. 1D TE and TM has 3 elements (TE has Ey, Hx and Hz in order and TM has Hy, Ex and Ez)
while the others have 6 elements (Ex, Ey, Ez, Hx, Hy and Hz) as in Figure 5.













