You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The fdtd library allows to choose a backend. The "numpy" backend is the
default one, but there are also several additional PyTorch backends:
"numpy" (defaults to float64 arrays)
"torch" (defaults to float64 tensors)
"torch.float32"
"torch.float64"
"torch.cuda" (defaults to float64 tensors)
"torch.cuda.float32"
"torch.cuda.float64"
For example, this is how to choose the "torch" backend:
fdtd.set_backend("torch")
In general, the "numpy" backend is preferred for standard CPU calculations
with "float64" precision. In general, "float64" precision is always
preferred over "float32" for FDTD simulations, however, "float32" might
give a significant performance boost.
The "cuda" backends are only available for computers with a GPU.
A grid is defined by its shape, which is just a 3D tuple of Number-types
(integers or floats). If the shape is given in floats, it denotes the width,
height and length of the grid in meters. If the shape is given in integers, it
denotes the width, height and length of the grid in terms of the
grid_spacing. Internally, these numbers will be translated to three integers:
grid.Nx, grid.Ny and grid.Nz.
A grid_spacing can be given. For stability reasons, it is recommended to
choose a grid spacing that is at least 10 times smaller than the smallest
wavelength in the grid. This means that for a grid containing a source with
wavelength 1550nm and a material with refractive index of 3.1, the
recommended minimum grid_spacing turns out to be 50nm
For the permittivity and permeability floats or arrays with the following
shapes
(grid.Nx, grid.Ny, grid.Nz)
or (grid.Nx, grid.Ny, grid.Nz, 1)
or (grid.Nx, grid.Ny, grid.Nz, 3)
are expected. In the last case, the shape implies the possibility for different
permittivity for each of the major axes (so-called uniaxial or biaxial
materials). Internally, these variables will be converted (for performance
reasons) to their inverses grid.inverse_permittivity array and a
grid.inverse_permeability array of shape (grid.Nx, grid.Ny, grid.Nz, 3). It
is possible to change those arrays after making the grid.
Finally, the courant_number of the grid determines the relation between the
time_step of the simulation and the grid_spacing of the grid. If not given,
it is chosen to be the maximum number allowed by the Courant-Friedrichs-Lewy
Condition:
1 for 1D simulations, 1/2 for 2D simulations and 1/3 for 3D
simulations (the dimensionality will be derived by the shape of the grid). For
stability reasons, it is recommended not to change this value.
grid=fdtd.Grid( shape= (25e-6, 15e-6, 1), # 25um x 15um x 1 (grid_spacing) --> 2D FDTD ) print(grid)
An object defines a part of the grid with modified update equations, allowing
to introduce for example absorbing materials or biaxial materials for which
mixing between the axes are present through Pockels coefficients or many
more. In this case we'll make an object with a different permittivity than
the grid it is in.
Just like for the grid, the Object expects a permittivity to be a floats or
an array of the following possible shapes
(obj.Nx, obj.Ny, obj.Nz)
or (obj.Nx, obj.Ny, obj.Nz, 1)
or (obj.Nx, obj.Ny, obj.Nz, 3)
Note that the values obj.Nx, obj.Ny and obj.Nz are not given to the
object constructor. They are in stead derived from its placing in the grid:
Several things happen here. First of all, the object is given the space
[11:32, 30:84, 0] in the grid. Because it is given this space, the object's
Nx, Ny and Nz are automatically set. Furthermore, by supplying a name to
the object, this name will become available in the grid:
Here, a slice with floating point numbers was chosen. These floats will be
replaced by integer Nx, Ny and Nz during the registration of the object.
Since the object did not receive a name, the object won't be available as an
attribute of the grid. However, it is still available via the grid.objects
list:
print(grid.objects)
[Object(name='object'), Object(name=None)]
This list stores all objects (i.e. of type fdtd.Object) in the order that
they were added to the grid.
Adding a source to the grid
Similarly as to adding an object to the grid, an fdtd.LineSource can also be
added:
However, it is important to note that in this case a LineSource is added to
the grid, i.e. the source spans the diagonal of the cube defined by the slices.
Internally, these slices will be converted into lists to ensure this behavior:
Note that one could also have supplied lists to index the grid in the first
place. This feature could be useful to create a LineSource of arbitrary
shape.
Adding a detector to the grid
# signature fdtd.LineDetector( name=None )
Adding a detector to the grid works the same as adding a source
Although, having an object, source and detector to simulate is in principle
enough to perform an FDTD simulation, One also needs to define a grid boundary
to prevent the fields to be reflected. One of those boundaries that can be
added to the grid is a Perfectly Matched
Layer or PML. These
are basically absorbing boundaries.
Just like for the lengths in the grid, the total_time of the simulation
can be specified as an integer (number of time_steps) or as a float (in
seconds).
grid.run(total_time=100)
Grid visualization
Let's visualize the grid. This can be done with the grid.visualize method:
This method will by default visualize all objects in the grid, as well as the
field intensity at the current time_step at a certain x, yORz-plane. By
setting show=False, one can disable the immediate visualization of the
matplotlib image.
grid.visualize(z=0)
Background
An as quick as possible explanation of the FDTD discretization of the Maxwell
equations.
Update Equations
An electromagnetic FDTD solver solves the time-dependent Maxwell Equations
curl(H) =e*e0*dE/dt curl(E) =-u*u0*dH/dt
These two equations are called Ampere's Law and Faraday's Law respectively.
In these equations, e and u are the relative permittivity and permeability
tensors respectively. e0 and u0 are the vacuum permittivity and permeability
and their square root can be absorbed into E and H respectively, such that E := e0*E and H := u0*H.
Doing this, the Maxwell equations can be written as update equations:
E+=c*dt*inv(e)*curl(H) H-=c*dt*inv(u)*curl(E)
The electric and magnetic field can then be discretized on a grid with
interlaced Yee-coordinates, which in 3D looks like this:
According to the Yee discretization algorithm, there are inherently two types
of fields on the grid: E-type fields on integer grid locations and H-type
fields on half-integer grid locations.
The beauty of these interlaced coordinates is that they enable a very natural
way of writing the curl of the electric and magnetic fields: the curl of an
H-type field will be an E-type field and vice versa.
The number (c*dt/du) is a dimensionless parameter called the Courant numbersc. For stability reasons, the Courant number should always be smaller than
1/D, with D the dimension of the simulation. This can be intuitively be
understood as the condition that information should always travel slower than
the speed of light through the grid. In the FDTD method described here,
information can only travel to the neighboring grid cells (through application
of the curl). It would therefore take D time steps to travel over the
diagonal of a D-dimensional cube (square in 2D, cube in 3D), the Courant
condition follows then automatically from the fact that the length of this
diagonal is 1/D.
This yields the final update equations for the FDTD algorithm:
Ampere's Law can be updated to incorporate a current density:
curl(H) =J+e*e0*dE/dt
Making again the usual substitutions sc := c*dt/du, E := e0*E and H := u0*H, the update equations can be modified to include the current density:
E+=sc*inv(e)*curl_H-dt*inv(e)*J/e0
Making one final substitution Es := -dt*inv(e)*J/e0 allows us to write this
in a very clean way:
E+=sc*inv(e)*curl_H+Es
Where we defined Es as the electric field source term.
It is often useful to also define a magnetic field source termHs, which would be
derived from the magnetic current density if it were to exist. In the same way,
Faraday's update equation can be rewritten as
H-=sc*inv(u)*curl_E+Hs
classSource: # ... [initialization] defupdate_E(self): # electric source function here
defupdate_H(self): # magnetic source function here
defupdate_H(self): # ... [magnetic field update equation] forsourceinself.sources: source.update_H()
Lossy Medium
When a material has a electric conductivity s, a conduction-current will
ensure that the medium is lossy. Ampere's law with a conduction current becomes
This update equation depends on the electric field on a half-integer time step (a
magnetic field time step). We need to substitute E(t+dt/2)=(E(t)+E(t+dt))/2 to
interpolate the electric field to the correct time step.
Note that the more complicated the permittivity tensor e is, the more time
consuming this algorithm will be. It is therefore sometimes a nice hack to
transfer the absorption to the magnetic domain by introducing a
(nonphysical) magnetic conductivity, because the permeability tensor u is
usually just equal to one:
The electric term and magnetic term in the energy density are usually of the
same size. Therefore, the same amount of energy will be absorbed by introducing
a magnetic conductivity sm as by introducing a electric conductivity s if:
inv(u)*sm/u0=inv(e)*s/e0
Boundary Conditions
Periodic Boundary Conditions
Assuming we want periodic boundary conditions along the X-direction, then we
have to make sure that the fields at Xlow and Xhigh are the same. This has
to be enforced after performing the update equations:
Note that the electric field E is dependent on curl_H, which means that the
first indices of E will not be updated through the update equations. It's
those indices that need to be set through the periodic boundary condition.
Concretely: E[0] needs to be set to equal E[-1]. For the magnetic field,
the inverse is true: H is dependent on curl_E, which means that its last
indices will not be set. This has to be done by the boundary condition: H[-1]
needs to be set equal to H[0]:
classGrid: # ... [initialization] defupdate_E(self): # ... [electric field update equation] # ... [electric field source update equations] forboundaryinself.boundaries: boundary.update_E()
defupdate_H(self): # ... [magnetic field update equation] # ... [magnetic field source update equations] forboundaryinself.boundaries: boundary.update_H()
Perfectly Matched Layer
a Perfectly Matched Layer (PML) is the state of the art for
introducing absorbing boundary conditions in an FDTD grid.
A PML is an impedance-matched absorbing area in the grid. It turns out that
for a impedance-matching condition to hold, the PML can only be absorbing in
a single direction. This is what makes a PML in fact a nonphysical material.
Consider Ampere's law for the Ez component, where we use the following substitutions:
E := e0*E, H := u0*H and s := inv(e)*s/e0 are
already introduced:
e*dEz/dt+e*s*Ez=c*dHy/dx-c*dHx/dy
This becomes in the frequency domain:
io*e*Ez+e*s*Ez=c*dHy/dx-c*dHx/dy
We can split this equation in a x-propagating wave and a y-propagating wave:
In general, we prefer to add a stability factor au and a scaling factor ku to Su:
Su=ku+su/(io+au) withuin {x, y, z}
Summing the two equations for Ez back together after dividing by the respective S-operator gives
io*e*Ez= (c/Sx)*dHy/dx- (c/Sy)*dHx/dy
Converting this back to the time domain gives
e*dEz/dt=c*sx[*]dHy/dx-c*sx[*]dHx/dy
where sx denotes the inverse Fourier transform of (1/Sx) and [*] denotes a convolution.
The expression for su can be proven [after some derivation] to look as follows:
su= (1/ku)*d(t) +Cu(t) withuin {x, y, z}
where d(t) denotes the Dirac delta function and C(t) an exponentially
decaying function given by:
Cu(t) =-(su/ku**2)*exp(-(au+su/ku)*t) forallt>0anduin {x, y, z}
As a bare FDTD library, this is dimensionally agnostic for any unit system you may choose.
No conversion factors are applied within the library API; this is left to the user.
(The code used to calculate the Courant limit may be a sticking point depending on the time scale involved).
However, as noted above (H := u0*H), it is generally good numerical practice to scale all values to
get the maximum precision from floating-point types.