Giter Site home page Giter Site logo

flaport / fdtd Goto Github PK

View Code? Open in Web Editor NEW
435.0 22.0 114.0 388 KB

A 3D electromagnetic FDTD simulator written in Python with optional GPU support

Home Page: https://fdtd.readthedocs.io

License: MIT License

Python 99.77% Makefile 0.23%
simulation simulation-framework fdtd optics photonics physics physics-simulation fdtd-simulator python pytorch numpy 3d-fdtd electric-fields magnetic-fields

fdtd's Introduction

Python 3D FDTD Simulator

Docs

A 3D electromagnetic FDTD simulator written in Python. The FDTD simulator has an optional PyTorch backend, enabling FDTD simulations on a GPU.

Installation

The fdtd-library can be installed with pip:

pip install fdtd

The development version can be installed by cloning the repository

git clone http://github.com/flaport/fdtd

and linking it with pip

pip install -e fdtd

Development dependencies can be installed with

pip install -e fdtd[dev]

Dependencies

  • python 3.6+
  • numpy
  • scipy
  • matplotlib
  • tqdm
  • pytorch (optional)

Contributing

All improvements or additions (for example new objects, sources or detectors) are welcome. Please make a pull-request 😊.

Documentation

read the documentation here: https://fdtd.readthedocs.org

Imports

The fdtd library is simply imported as follows:

import fdtd

Setting the backend

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.

The FDTD-grid

The FDTD grid defines the simulation region.

# signature
fdtd.Grid(
    shape: Tuple[Number, Number, Number],
    grid_spacing: float = 155e-9,
    permittivity: float = 1.0,
    permeability: float = 1.0,
    courant_number: float = None,
)

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)
Grid(shape=(161,97,1), grid_spacing=1.55e-07, courant_number=0.70)

Adding an object to the grid

An other option to locally change the permittivity or permeability in the grid is to add an Object to the grid.

# signature
fdtd.Object(
    permittivity: Tensorlike,
    name: str = None
)

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:

grid[11:32, 30:84, 0] = fdtd.Object(permittivity=1.7**2, name="object")

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:

print(grid.object)
    Object(name='object')
        @ x=11:32, y=30:84, z=0:1

A second object can be added to the grid:

grid[13e-6:18e-6, 5e-6:8e-6, 0] = fdtd.Object(permittivity=1.5**2)

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:

# signature
fdtd.LineSource(
    period: Number = 15, # timesteps or seconds
    amplitude: float = 1.0,
    phase_shift: float = 0.0,
    name: str = None,
)

And also just like an fdtd.Object, an fdtd.LineSource size is defined by its placement on the grid:

grid[7.5e-6:8.0e-6, 11.8e-6:13.0e-6, 0] = fdtd.LineSource(
    period = 1550e-9 / (3e8), name="source"
)

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:

print(grid.source)
    LineSource(period=14, amplitude=1.0, phase_shift=0.0, name='source')
        @ x=[48, ... , 51], y=[76, ... , 83], z=[0, ... , 0]

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

grid[12e-6, :, 0] = fdtd.LineDetector(name="detector")
print(grid.detector)
    LineDetector(name='detector')
        @ x=[77, ... , 77], y=[0, ... , 96], z=[0, ... , 0]

Adding grid boundaries

# signature
fdtd.PML(
    a: float = 1e-8, # stability factor
    name: str = None
)

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.

# x boundaries
grid[0:10, :, :] = fdtd.PML(name="pml_xlow")
grid[-10:, :, :] = fdtd.PML(name="pml_xhigh")

# y boundaries
grid[:, 0:10, :] = fdtd.PML(name="pml_ylow")
grid[:, -10:, :] = fdtd.PML(name="pml_yhigh")

Grid summary

A simple summary of the grid can be shown by printing out the grid:

print(grid)
Grid(shape=(161,97,1), grid_spacing=1.55e-07, courant_number=0.70)

sources:
    LineSource(period=14, amplitude=1.0, phase_shift=0.0, name='source')
        @ x=[48, ... , 51], y=[76, ... , 83], z=[0, ... , 0]

detectors:
    LineDetector(name='detector')
        @ x=[77, ... , 77], y=[0, ... , 96], z=[0, ... , 0]

boundaries:
    PML(name='pml_xlow')
        @ x=0:10, y=:, z=:
    PML(name='pml_xhigh')
        @ x=-10:, y=:, z=:
    PML(name='pml_ylow')
        @ x=:, y=0:10, z=:
    PML(name='pml_yhigh')
        @ x=:, y=-10:, z=:

objects:
    Object(name='object')
        @ x=11:32, y=30:84, z=0:1
    Object(name=None)
        @ x=84:116, y=32:52, z=0:1

Running a simulation

Running a simulation is as simple as using the grid.run method.

grid.run(
    total_time: Number,
    progress_bar: bool = True
)

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:

# signature
grid.visualize(
    grid,
    x=None,
    y=None,
    z=None,
    cmap="Blues",
    pbcolor="C3",
    pmlcolor=(0, 0, 0, 0.1),
    objcolor=(1, 0, 0, 0.1),
    srccolor="C0",
    detcolor="C2",
    show=True,
)

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, y OR z-plane. By setting show=False, one can disable the immediate visualization of the matplotlib image.

grid.visualize(z=0)

png

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) = ε*ε0*dE/dt
    curl(E) = -µ*µ0*dH/dt

These two equations are called Ampere's Law and Faraday's Law respectively.

In these equations, ε and µ are the relative permittivity and permeability tensors respectively. ε0 and µ0 are the vacuum permittivity and permeability and their square root can be absorbed into E and H respectively, such that E := √ε0*E and H := √µ0*H.

Doing this, the Maxwell equations can be written as update equations:

    E  += c*dt*inv(ε)*curl(H)
    H  -= c*dt*inv(µ)*curl(E)

The electric and magnetic field can then be discretized on a grid with interlaced Yee-coordinates, which in 3D looks like this:

grid discretization in 3D

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.

This way, the curl of E can be written as

    curl(E)[m,n,p] = (dEz/dy - dEy/dz, dEx/dz - dEz/dx, dEy/dx - dEx/dy)[m,n,p]
                   =( ((Ez[m,n+1,p]-Ez[m,n,p])/dy - (Ey[m,n,p+1]-Ey[m,n,p])/dz),
                      ((Ex[m,n,p+1]-Ex[m,n,p])/dz - (Ez[m+1,n,p]-Ez[m,n,p])/dx),
                      ((Ey[m+1,n,p]-Ey[m,n,p])/dx - (Ex[m,n+1,p]-Ex[m,n,p])/dy) )
                   =(1/du)*( ((Ez[m,n+1,p]-Ez[m,n,p]) - (Ey[m,n,p+1]-Ey[m,n,p])), [assume dx=dy=dz=du]
                             ((Ex[m,n,p+1]-Ex[m,n,p]) - (Ez[m+1,n,p]-Ez[m,n,p])),
                             ((Ey[m+1,n,p]-Ey[m,n,p]) - (Ex[m,n+1,p]-Ex[m,n,p])) )

this can be written efficiently with array slices (note that the factor (1/du) was left out):

def curl_E(E):
    curl_E = np.zeros(E.shape)
    curl_E[:,:-1,:,0] += E[:,1:,:,2] - E[:,:-1,:,2]
    curl_E[:,:,:-1,0] -= E[:,:,1:,1] - E[:,:,:-1,1]

    curl_E[:,:,:-1,1] += E[:,:,1:,0] - E[:,:,:-1,0]
    curl_E[:-1,:,:,1] -= E[1:,:,:,2] - E[:-1,:,:,2]

    curl_E[:-1,:,:,2] += E[1:,:,:,1] - E[:-1,:,:,1]
    curl_E[:,:-1,:,2] -= E[:,1:,:,0] - E[:,:-1,:,0]
    return curl_E

The curl for H can be obtained in a similar way (note again that the factor (1/du) was left out):

def curl_H(H):
    curl_H = np.zeros(H.shape)

    curl_H[:,1:,:,0] += H[:,1:,:,2] - H[:,:-1,:,2]
    curl_H[:,:,1:,0] -= H[:,:,1:,1] - H[:,:,:-1,1]

    curl_H[:,:,1:,1] += H[:,:,1:,0] - H[:,:,:-1,0]
    curl_H[1:,:,:,1] -= H[1:,:,:,2] - H[:-1,:,:,2]

    curl_H[1:,:,:,2] += H[1:,:,:,1] - H[:-1,:,:,1]
    curl_H[:,1:,:,2] -= H[:,1:,:,0] - H[:,:-1,:,0]
    return curl_H

The update equations can now be rewritten as

    E  += (c*dt/du)*inv(ε)*curl_H
    H  -= (c*dt/du)*inv(µ)*curl_E

The number (c*dt/du) is a dimensionless parameter called the Courant number sc. 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:

    E  += sc*inv(ε)*curl_H
    H  -= sc*inv(µ)*curl_E

This is also how it is implemented:

class Grid:
    # ... [initialization]

    def step(self):
        self.update_E()
        self.update_H()

    def update_E(self):
        self.E += self.courant_number * self.inverse_permittivity * curl_H(self.H)

    def update_H(self):
        self.H -= self.courant_number * self.inverse_permeability * curl_E(self.E)

Sources

Ampere's Law can be updated to incorporate a current density:

    curl(H) = J + ε*ε0*dE/dt

Making again the usual substitutions sc := c*dt/du, E := √ε0*E and H := √µ0*H, the update equations can be modified to include the current density:

    E += sc*inv(ε)*curl_H - dt*inv(ε)*J/ε0

Making one final substitution Es := -dt*inv(ε)*J/√ε0 allows us to write this in a very clean way:

    E += sc*inv(ε)*curl_H + Es

Where we defined Es as the electric field source term.

It is often useful to also define a magnetic field source term Hs, 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(µ)*curl_E + Hs
class Source:
    # ... [initialization]
    def update_E(self):
        # electric source function here

    def update_H(self):
        # magnetic source function here

class Grid:
    # ... [initialization]
    def update_E(self):
        # ... [electric field update equation]
        for source in self.sources:
            source.update_E()

    def update_H(self):
        # ... [magnetic field update equation]
        for source in self.sources:
            source.update_H()

Lossy Medium

When a material has a electric conductivity σ, a conduction-current will ensure that the medium is lossy. Ampere's law with a conduction current becomes

    curl(H) = σ*E + ε*ε0*dE/dt

Making the usual substitutions, this becomes:

    E(t+dt) - E(t) = sc*inv(ε)*curl_H(t+dt/2) - dt*inv(ε)*σ*E(t+dt/2)/ε0

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.

    (1 + 0.5*dt*inv(ε)*σ/ε0)*E(t+dt) = sc*inv(ε)*curl_H(t+dt/2) + (1 - 0.5*dt*inv(ε)*σ/ε0)*E(t)

Which, yield the new update equations:

    f = 0.5*inv(ε)*σ*sc*du/(ε0*c)
    E *= inv(1 + f) * (1 - f)
    E += inv(1 + f)*sc*inv(ε)*curl_H

Note that the more complicated the permittivity tensor ε 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 µ is usually just equal to one:

    f = 0.5*inv(μ)*σm*sc*du/(μ0*c)
    H *= inv(1 + f) * (1 - f)
    H += inv(1 + f)*sc*inv(µ)*curl_E

Energy Density and Poynting Vector

The electromagnetic energy density can be given by

    e = (1/2)*ε*ε0*E**2 + (1/2)*µ*µ0*H**2

making the above substitutions, this becomes in simulation units:

    e = (1/2)*ε*E**2 + (1/2)*µ*H**2

The Poynting vector is given by

    P = E×H

Which in simulation units becomes

    P = c*E×H

The energy introduced by a source Es can be derived from tracking the change in energy density

    de = ε*Es·E + (1/2)*ε*Es**2

This could also be derived from Poyntings energy conservation law:

    de/dt = -grad(S) - J·E

where the first term just describes the redistribution of energy in a volume and the second term describes the energy introduced by a current density.

Note: although it is unphysical, one could also have introduced a magnetic source. This source would have introduced the following energy:

    de = ε*Hs·H + (1/2)*µ*Hs**2

Since the µ-tensor is usually just equal to one, using a magnetic source term is often more efficient.

Similarly, one can also keep track of the absorbed energy due to an electric conductivity in the following way:

    f = 0.5*inv(ε)*σ*sc*du/(ε0*c)
    Enoabs = E + sc*inv(ε)*curl_H
    E *= inv(1 + f) * (1 - f)
    E += inv(1 + f)*sc*inv(ε)*curl_H
    dE = Enoabs - E
    e_abs += ε*E*dE + 0.5*ε*dE**2

or if we want to keep track of the absorbed energy by magnetic a magnetic conductivity:

    f = 0.5*inv(μ)*σm*sc*du/(μ0*c)
    Hnoabs = E + sc*inv(µ)*curl_E
    H *= inv(1 + f) * (1 - f)
    H += inv(1 + f)*sc*inv(µ)*curl_E
    dH = Hnoabs - H
    e_abs += µ*H*dH + 0.5*µ*dH**2

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 σm as by introducing a electric conductivity σ if:

    inv(µ)*σm/µ0 = inv(ε)*σ/ε0

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]:

class PeriodicBoundaryX:
    # ... [initialization]
    def update_E(self):
        self.grid.E[0, :, :, :] = self.grid.E[-1, :, :, :]

    def update_H(self):
        self.grid.H[-1, :, :, :] = self.grid.H[0, :, :, :]

class Grid:
    # ... [initialization]
    def update_E(self):
        # ... [electric field update equation]
        # ... [electric field source update equations]
        for boundary in self.boundaries:
            boundary.update_E()

    def update_H(self):
        # ... [magnetic field update equation]
        # ... [magnetic field source update equations]
        for boundary in self.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 := √ε0*E, H := √µ0*H and σ := inv(ε)*σ/ε0 are already introduced:

    ε*dEz/dt + ε*σ*Ez = c*dHy/dx - c*dHx/dy

This becomes in the frequency domain:

    *ε*Ez + ε*σ*Ez = c*dHy/dx - c*dHx/dy

We can split this equation in a x-propagating wave and a y-propagating wave:

    *ε*Ezx + ε*σx*Ezx = *ε*(1 + σx/)*Ezx = c*dHy/dx
    *ε*Ezy + ε*σy*Ezy = *ε*(1 + σy/)*Ezy = -c*dHx/dy

We can define the S-operators as follows

    Su = 1 + σu/          with u in {x, y, z}

In general, we prefer to add a stability factor au and a scaling factor ku to Su:

    Su = ku + σu/(+au)    with u in {x, y, z}

Summing the two equations for Ez back together after dividing by the respective S-operator gives

    *ε*Ez = (c/Sx)*dHy/dx - (c/Sy)*dHx/dy

Converting this back to the time domain gives

    ε*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)*δ(t) + Cu(t)    with u in {x, y, z}

where δ(t) denotes the Dirac delta function and C(t) an exponentially decaying function given by:

    Cu(t) = -(σu/ku**2)*exp(-(au+σu/ku)*t)     for all t > 0 and u in {x, y, z}

Plugging this in gives:

    dEz/dt = (c/kx)*inv(ε)*dHy/dx - (c/ky)*inv(ε)*dHx/dy + c*inv(ε)*Cx[*]dHy/dx - c*inv(ε)*Cx[*]dHx/dy
           = (c/kx)*inv(ε)*dHy/dx - (c/ky)*inv(ε)*dHx/dy + c*inv(ε)*Фez/du      with du=dx=dy=dz

This can be written as an update equation:

    Ez += (1/kx)*sc*inv(ε)*dHy - (1/ky)*sc*inv(ε)*dHx + sc*inv(ε)*Фez

Where we defined Фeu as

    Фeu = Ψeuv - Ψezw           with u, v, w in {x, y, z}

and Ψeuv as the convolution updating the component Eu by taking the derivative of Hw in the v direction:

    Ψeuv = dv*Cv[*]dHw/dv     with u, v, w in {x, y, z}

This can be rewritten [after some derivation] as an update equation in itself:

     Ψeuv = bv*Ψeuv + cv*dv*(dHw/dv)
          = bv*Ψeuv + cv*dHw            with u, v, w in {x, y, z}

Where the constants bu and cu are derived to be:

    bu = exp(-(au + σu/ku)*dt)              with u in {x, y, z}
    cu = σu*(bu - 1)/(σu*ku + au*ku**2)     with u in {x, y, z}

The final PML algorithm for the electric field now becomes:

  1. Update Фe=[Фex, Фey, Фez] by using the update equation for the Ψ-components.
  2. Update the electric fields the normal way
  3. Add Фe to the electric fields.

or as python code:

class PML(Boundary):
    # ... [initialization]
    def update_phi_E(self): # update convolution
        self.psi_Ex *= self.bE
        self.psi_Ey *= self.bE
        self.psi_Ez *= self.bE

        c = self.cE
        Hx = self.grid.H[self.locx]
        Hy = self.grid.H[self.locy]
        Hz = self.grid.H[self.locz]

        self.psi_Ex[:, 1:, :, 1] += (Hz[:, 1:, :] - Hz[:, :-1, :]) * c[:, 1:, :, 1]
        self.psi_Ex[:, :, 1:, 2] += (Hy[:, :, 1:] - Hy[:, :, :-1]) * c[:, :, 1:, 2]

        self.psi_Ey[:, :, 1:, 2] += (Hx[:, :, 1:] - Hx[:, :, :-1]) * c[:, :, 1:, 2]
        self.psi_Ey[1:, :, :, 0] += (Hz[1:, :, :] - Hz[:-1, :, :]) * c[1:, :, :, 0]

        self.psi_Ez[1:, :, :, 0] += (Hy[1:, :, :] - Hy[:-1, :, :]) * c[1:, :, :, 0]
        self.psi_Ez[:, 1:, :, 1] += (Hx[:, 1:, :] - Hx[:, :-1, :]) * c[:, 1:, :, 1]

        self.phi_E[..., 0] = self.psi_Ex[..., 1] - self.psi_Ex[..., 2]
        self.phi_E[..., 1] = self.psi_Ey[..., 2] - self.psi_Ey[..., 0]
        self.phi_E[..., 2] = self.psi_Ez[..., 0] - self.psi_Ez[..., 1]

    def update_E(self): # update PML located at self.loc
        self.grid.E[self.loc] += (
            self.grid.courant_number
            * self.grid.inverse_permittivity[self.loc]
            * self.phi_E
        )

class Grid:
    # ... [initialization]
    def update_E(self):
        for boundary in self.boundaries:
            boundary.update_phi_E()
        # ... [electric field update equation]
        # ... [electric field source update equations]
        for boundary in self.boundaries:
            boundary.update_E()

The same has to be applied for the magnetic field.

These update equations for the PML were based on Schneider, Chap. 11.

Units

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 := õ0*H), it is generally good numerical practice to scale all values to get the maximum precision from floating-point types.

In particular, a scaling scheme detailed in "Novel architectures for brain-inspired photonic computers", Chapters 4.1.2 and 4.1.6, is highly recommended.

A set of conversion functions to and from reduced units are available for users in conversions.py.

Linter

You can run a linter in the root using pylint fdtd.

License

© Floris laporte - MIT License

fdtd's People

Contributors

0xdbfb7 avatar devbrones avatar flaport avatar hajanssen avatar hera-observer avatar iakovts avatar per-gron avatar substancia avatar ttstone avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fdtd's Issues

Overlapping Source inside Object

I was trying to build a (dubious) test-case for current density by putting a source in a conductive medium, like so:

grid[x_,y_,z_] = fdtd.AbsorbingObject(permittivity=1.0, conductivity=conductivity)
grid[x_,y_,z_] = fdtd.PointSource(period=500)

The source E-field seems to always be forced to zero in this case. The source works when the objects are added in the other order.

I might be doing something wrong, just posting this issue to ask what the expected behavior should be when troubleshooting - is the overlapping case allowed and meaningful?

Thanks!

(tangentially related, should adding an object inside a PML maybe throw an exception? Do you know of any cases where that would be a valid thing to do?)

Mode source

The library could use a mode source for some integrated photonics simulations. This probably requires a [simple] 2D mode solver dependency/addition to the library.

Polarization of plane source

Hi,
let me first say this repo is quite unique and phantastic!

My question: when using plane sources it seems that something is weird with the polarization, I got an error: TypeError: init() got an unexpected keyword argument 'polarization'.
Should be in the init function https://github.com/flaport/fdtd/blob/master/fdtd/sources.py#L326

For example, this code snippet throws the error:
grid[0, :, :] = fdtd.PlaneSource(
period=WAVELENGTH / SPEED_LIGHT, polarization='z')

I installed via 'pip install fdtd' couple of days ago.

Am I right assuming that default polarization is still the z-axis?

moving source

Dear author, many thanks for your interesting package! In my problem I have to study the field of a source moving with the uniform velocity along the grid. Is there any way to indicate the current position of the point source (depending on current time) while the field update? Thank you in advance.

General Tasks

Hello again @flaport,

I 've been reading through the code, and I have suggestions (that I could also possibly implement):

  1. Automate tests through github Actions. (I ve actually already implemented a PoC on my fork - will open a PR). In the future Actions can also be used for publishing docs and the package itself on pypi.

  2. Some (minor?) code refactoring, hopefully without having to change the current API at all. An example of what I have in mind is changing this to a class factory and generally some clarity changes.

  3. [edit] I would also suggest using another branch for active development/merges were changes are frequent (dev/staging?) and using master as a definitive branch for releases.

Looking forward to your thoughts on the above and more :)

removing objects doesn't reset grid permittivity

Very cool project! I look forward to seeing where it goes.

Small observation here, after playing around a little bit:
When you add an object, and then later remove it, you end up with a section of the grid with altered permittivity, but without an object present in the visualization. Lacking a grid.remove_object() method, I naively just popped the object from grid.objects... and used grid.reset(). but obviously, that doesn't undo the changes to the grid that occurred when the object was registered.

I'd definitely understand if support for removing items (rather than just re-instantiating a new grid) was low priority for now, but just figured I'd mention it.
thanks again!

Field in position of source

Hi @flaport, Happy New Year! At FDTD calculations I need to periodically change the power of a source (initially F_0) dependently on the local field amplitude E_0. So first I have to know the amplitude of field E_0 in current position of a source in 3D. Latter I will modify the power of this source periodically as F_{n+1} = f(E_n)*F_n with the interval dt. As I understand, to do that I have to periodically remove old source (n) from the grid and insert in this position new source (n+1). What is the optimal way to do all of that? Thank you in advance.

example.py not compiling

AttributeError: module 'fdtd' has no attribute 'Detectors'

changing fdtd.Detectors to fdtd.LineDetector seems to solve the problem but raises another problem:

ValueError: at least one projection plane (x, y or z) should be supplied to visualize the grid!

Version on pypi doesn't have commit "don't use bd.float directly for type casting" and is erroring

Version 0.2.6 on pypi doesn't have the commit 3cb63c27455e3f038dbd26b1394549a156 to "don't use bd.float directly for type casting". Without this commit, the initialization of a grid with any of the "torch" backends errors:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 grid = fdtd.Grid(
      2     (2.5e-5, 1.5e-5, 1),
      3     grid_spacing=0.01 * WAVELENGTH,
      4     permittivity=1.0,
      5     permeability=1.0,
      6 )

File ~/projects/compem/venv/lib/python3.10/site-packages/fdtd/grid.py:143, in Grid.__init__(self, shape, grid_spacing, permittivity, permeability, courant_number)
    141 if bd.is_array(permittivity) and len(permittivity.shape) == 3:
    142     permittivity = permittivity[:, :, :, None]
--> 143 self.inverse_permittivity = bd.ones((self.Nx, self.Ny, self.Nz, 3)) / bd.float(
    144     permittivity
    145 )
    147 if bd.is_array(permeability) and len(permeability.shape) == 3:
    148     permeability = permeability[:, :, :, None]

TypeError: 'torch.dtype' object is not callable

Do you plan to push a new version to pypi?

python 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0]
torch 1.13.1+cu117

`RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!`

I've tried a minimal example:

wavelength = 1e-6
speed_of_light = 277972458

# Initialize FDTD grid
grid = fdtd.Grid(shape=(20*wavelength, 50*wavelength, 1), grid_spacing=wavelength/10)

# Initialize perfectly matched layer (PML) boundaries
grid[0:10, :, :] = fdtd.PML(name="pml_xlow")
grid[-10:, :, :] = fdtd.PML(name="pml_xhigh")
grid[:, 0:10, :] = fdtd.PML(name="pml_ylow")
grid[:, -10:, :] = fdtd.PML(name="pml_yhigh")

# Initialize source
grid[:, 10, 0] = fdtd.LineSource(period=wavelength/speed_of_light, name="source")

# Initialize objects
#grid[10:190, 50:100, :] = fdtd.AbsorbingObject(permittivity=2.5, conductivity=1e-6, name="absorbing_object")

# Run simulation
start_time = timeit.default_timer()

for i in range(200):
    grid.step()  # Run simulation one timestep at a time and animate
    if i % 10 == 0:
        grid.visualize(z=0, cmap='plasma', animate=True, index=i, save=False)
        plt.title(f"Simulation step {i}")
        #clear_output(wait=True)  # Only necessary in Jupyter notebooks

print(timeit.default_timer() - start_time)

But I'm getting this runtime error:

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Input In [51], in <cell line: 17>()
     13 grid[:, 10, 0] = fdtd.LineSource(period=wavelength/299792458, name="source")
     16 # Initialize objects
---> 17 grid[10:190, 50:100, :] = fdtd.AbsorbingObject(permittivity=2.5, conductivity=1e-6, name="absorbing_object")
     20 # Run simulation
     21 start_time = timeit.default_timer()

File C:\Program Files\Python38\lib\site-packages\fdtd\grid.py:365, in Grid.__setitem__(self, key, attr)
    362 else:
    363     raise KeyError("maximum number of indices for the grid is 3")
--> 365 attr._register_grid(
    366     grid=self,
    367     x=self._handle_single_key(x),
    368     y=self._handle_single_key(y),
    369     z=self._handle_single_key(z),
    370 )

File C:\Program Files\Python38\lib\site-packages\fdtd\objects.py:193, in AbsorbingObject._register_grid(self, grid, x, y, z)
    187     conductivity = conductivity[..., None]
    188 self.conductivity = bd.broadcast_to(
    189     conductivity, self.inverse_permittivity.shape
    190 )
    192 self.absorption_factor = (
--> 193     0.5
    194     * self.grid.courant_number
    195     * self.inverse_permittivity
    196     * self.conductivity
    197     * self.grid.grid_spacing
    198     * const.eta0
    199 )

RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!

Flat Plane Source

Hi folks, thanks for the awesome fdtd package!
I'm trying to get a flat PlaneSource, eg a flat plane on z=N, but I haven't found a way to do it. Using something like grid[:,:,5]=fdtd.AnisotropicObject(permittivity=permittivity) gives a diagonal plane. Is there a way to get a flat plane? Thanks!

Frequency dependent permittivity

Hi, thank you very much for this awesome repository!

If I understand the code correctly at the moment it's not possible to use Objects with a frequency dependent permittivity (like gold nanoparticles for instance), is there any plan to support it?

Acoustic simulations?

Hello, I know this package is for 3D electromagnetic simulations, but I was wondering what it would take for it to also be used for acoustic simulations (including in situations where the speed of sound was not constant). Would this be something of interest to you, or is it too far outside the scope of this package? My elementary reading suggests that it shouldn't be too hard to have equations also for sound propagation work in this framework, but curious what you think.

new grid permittivity strategy & issues with PML and Objects

I fear I may have caused more problems in issue #4. It appears that commit 3e22a26 has introduced some new bugs relating to PMLs and certain objects. (either that or I'm using them incorrectly, and this commit exposed my errors)

To explain the issue, let me describe my simulation. I have a simply "TIR" example grid where the top half has a higher refractive index (1.55) than the bottom (1.33), an angled LineSource points at the middle of the grid, and a (somewhat strange) very high refractive index "absorbing" object serves to block stray energy on the left side at the interface, and PMLs surround the grid (note: the 1.55 RI region overlaps the PML):

grid layout

previously, this was the result after running for a while:
old_version

these lines in commit 3e22a26 now cause some strange (high magnitude E-field) results in both the PML and my weird high refractive index object at the interface:
new version

I can remove the boundary errors by taking extra care not to have any objects overlapping the PML regions, (though previously, fdtd handled this a bit more gracefully, and it didn't matter whether these overlapped). However, I still have problems with my "absorbing" object at the interface.
new_without_overriding_pml

it's possible that I'm just not implementing absorbing objects correctly, or that the object edge code just needs some tweaks ... but it also seems like there are generally more "gotchas" now with the new grid permittivity strategy and the PML than there were before you generously dealt with my request to remove objects :)

I tried for a while to figure out a PR to fix it gracefully while leaving the ability to remove objects, but so far have failed. I personally wouldn't mind if you wanted to rollback the grid removal idea for a while, if it's the easiest way to deal with this. I know you're busy with other things!

thanks again!

Objects of arbitrary shape

Thanks for your code! And I wonder how to define a structure like circle? More generally, how can I define an arbitrary shape? Thanks.

How should I implement pattern lighting?

Dear author, this framework is very convenient, but I need to solve the S parameter of some devices in use, but there is no Mode Source at present. Can you give me some help?Thank you!!!

Regarding the detected value

Dear Floris Laporte,

Thank you very much for the nice tool!

Recently, I am learning the package for simulation. I noticed that there is a 'detector_value()' function in the class 'detector', however there is not an example for the demonstration usage of it, could you please add an example on this function?

Paraview exporting with pyEVTK

When testing 3D geometries, it seemed pretty helpful to dump .vtk files with object and field grids. The pyEVTK lib makes this pretty clean, might perhaps be worth an optional dependency?

On the other hand, perhaps this is something best handled by user code...

Loading Spatial Distributions

When loading an array into the "permittivity" parameter, I get the following error:

TypeError: only size-1 arrays can be converted to Python scalars

from "...\fdtd\grid.py" line 159

I believe this is because it's trying to apply "float()" to a numpy array. Changing this to "bd.float()" appears to fix it for numpy, but I don't know if it is compatible with the others.

Just putting it on your radar.

Cheers!

Use 'quantities' library to handle conversions between simulation units and actual units

I'm wondering what the units used for each quantity are, both for parameters which are passed in and for values stored internally or produced by the calculation.

A lot of things seem to be unitless, but other things seem to have assumed units. A quick list:

  • grid.E - the docs say "E := √ε0*E" - does this mean that grid.E divided by sqrt(numeric value of epsilon0 expressed in farads/meter) (= grid.E/2.98e-6) would produce the E field in volts/meter? is this basically because sqrt(epsilon0)*sqrt(mu0) = speed of light?
  • grid.H?
  • Grid(grid_spacing) in meters?
  • grid.time_step in seconds?
  • AbsorbingObject(conductivity) in siemens/meter? (really not sure about this one but I need it to match specific materials)
  • PointSource(power) in watts?

It would be great to track all quantities with units and add this to the documentation. I can try to write that up as long as someone can help answer questions.

Frequency domain detector

The library could use a frequency domain detector. This can be implemented with a simple Fourier transform.

ffmpeg not installed?

None of your example files will generate a video on Windows.

The error ffmpeg not installed? is thrown despite ffmpeg-python being installed.

I looked up the relevant function generate_video() in your source code and it says Note: this function requires ``ffmpeg`` to be available in your path., so I've tried copying the ffmpeg package folder into the same folder as the .ipynb notebook containing your example simulations and even moving all .py files directly into it, but to no avail.

Could you please check what's wrong and, if necessary, make corrections so that fdtd will recognize the ffmpeg package?

Unit correction

I found this issue in the doc.
'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 50pm'

This 50 pm seems to be wrong, instead, it should be 50 nm.

Updated version of FDTD module

Hi,
I am a final year Masters student in Theoretical Physics at IIT Madras. When I started my final year project in August 2020, this was the best suiting cross-platform FDTD module I could find for my simulation project. In past months, I have added many features to this module to fit my purposes, like real-time simulation video, saving simulation video and detector history and grid details, Hanning window pulse sources etc. Once I wrap up my project in next two months, I would like to share my progress on your library and help the community grow.

Question regarding conductivity implementation in code

Hi,

I was browsing through objects.py when I came upon this, lines 192-199,

 self.absorption_factor = (
            0.5
            * self.grid.courant_number
            * self.inverse_permittivity
            * self.conductivity
            * self.grid.grid_spacing
            / const.eps0
        )

which is f = 0.5*sc*inv(ε)*σe*du/ε0
In https://github.com/flaport/fdtd#lossy-medium, the following is written:

f = 0.5*dt*σ, where σ := inv(ε)*σe/ε0

by taking dt = sc * du/c I can expand f = (0.5*sc*inv(ε)*σe*du)/(ε0*c)

My question is, why wasn't 1/speed of light, 1/c included in self.absorption_factor ? There must be something I'm missing, right?

Any clarification to my confusion would be most appreciated 😄

Tests

The library needs tests to prevent bugs. This library uses pytest as test suite, which is by far the most user friendly test-suite for Python. This is great, because this means anyone can add tests; it's super easy! Moreover, it makes you acquainted with the code, which is why it's labeled a good first issue.

When adding tests, keep the following in mind:

  • Add tests to the right file in the tests folder
  • Each test case should be a function with a name starting with test_
  • Try to test only one single thing (a single assert) in each test case
  • When repeating code over multiple tests, try to use pytest fixtures.
  • Try to make the name of what you're testing as explicit as possible
  • In case the function of a test is not clear from its name, add a docstring.
  • Try to set a value for each argument and keyword argument. The default values might (and probably will) change, setting the values explicitly probably increases the lifetime of a test.
  • Use black to typeset your code:
    pip install black
    black tests
  • Never test floats directly; always use pytest.approx and similar.
  • Use common sense values when creating tests.

When running the tests:

  • Make sure the fdtd library is in your path
    • The easiest way to make this happen is by installing the library via pip install -e:
          pip uninstall fdtd # uninstall any previous version of the fdtd library
          git clone https://github.com/flaport/fdtd # if not cloned yet, clone the repository now.
          pip install -e fdtd # install as a development library
      
    • When installed this way, pip will link this local version of the fdtd
      library into your python path; any change in this development library will
      from now on be reflected in the version you import by default with python.
    • Note that pip install -e is executed in the folder containing the git repository NOT inside the git repository.
  • Now, from inside the git repository, run
        pytest tests
    
  • Or, if you want a coverage report (requires pytest-cov)
        pytest tests --cov-report html --cov fdtd
    
    • The coverage report can be found in the newly created htmlcov folder.

Unit of conductivity

Hi @flaport, thanks for the excellent toolbox. Just a quick question, may I know the unit of conductivity defined in the absorbing object?
In your example (https://fdtd.readthedocs.io/en/latest/examples/02-absorbing-object.html#), the conductivity was set to 1e-6, but the unit was not provided.
I'm interested in a medium that has a conductivity of 0.6 ohm-1 m-1. However, if I set the conductivity to 0.6, the program gives a absorption_factor larger than 1. Could you give me any suggestions on setting the correct conductivity for your toolbox. Thanks!

Error using Cuda

Hello,

I had an issue using the "torch.cuda" backend. Adding an Object yields the following Error.

TypeError                                 Traceback (most recent call last)
[<ipython-input-7-87b12cc5ca33>](https://localhost:8080/#) in <module>()
     34 permittivity = np.ones((180,180,1))
     35 permittivity += circle_mask[:,:,None]*(refractive_index**2 - 1)
---> 36 grid[500-180//2:500+180//2, 500-180//2:500+180//2, 0] = fdtd.Object(permittivity=permittivity, name="object")
     37 
     38 

[/usr/local/lib/python3.7/dist-packages/fdtd/grid.py](https://localhost:8080/#) in __setitem__(self, key, attr)
    367             x=self._handle_single_key(x),
    368             y=self._handle_single_key(y),
--> 369             z=self._handle_single_key(z),
    370         )
    371 

[/usr/local/lib/python3.7/dist-packages/fdtd/objects.py](https://localhost:8080/#) in _register_grid(self, grid, x, y, z)
     67             self.permittivity = self.permittivity[:, :, :, None]
     68         self.inverse_permittivity = (
---> 69             bd.ones((self.Nx, self.Ny, self.Nz, 3)) / self.permittivity
     70         )
     71 

[/usr/local/lib/python3.7/dist-packages/torch/_tensor.py](https://localhost:8080/#) in __array__(self, dtype)
    730             return handle_torch_function(Tensor.__array__, (self,), self, dtype=dtype)
    731         if dtype is None:
--> 732             return self.numpy()
    733         else:
    734             return self.numpy().astype(dtype, copy=False)

TypeError: can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.

This happened on my local machine and on Google Colab.
As I have understood, the error happens, because self.permittivity in objects.py is not copied to the GPU.
For me, this can be mitigated with self.permittivity = bd.array(self.permittivity) in a previous line.

Greetings,
Hauke

Defining a 3D Plane Source

I'm trying to define a plane source such that it outputs a plane wave, but I'm getting some odd-looking outputs. The following code seems to produce some pretty heavy fringes of some sort. Any thoughts? Ideally it would produce a smooth plane wave.
Thanks!
plane_source


import matplotlib.pyplot as plt

import fdtd
import fdtd.backend as bd
import numpy as np
import torch
import torch.autograd
import torch.nn
fdtd.set_backend("torch.cuda")


WAVELENGTH = 1550e-9
SPEED_LIGHT: float = 299_792_458.0  # [m/s] speed of light


grid = fdtd.Grid(
    (2.5e-5, 2.5e-5, 1.5e-5),
    # (161,161,97),
    grid_spacing=0.1 * WAVELENGTH,
    permittivity=1.0,
    permeability=1.0,
    # courant_number=0.3
)

grid[0, :, :] = fdtd.PeriodicBoundary(name="xbounds")
# grid[0:10, :, :] = fdtd.PML(name="pml_xlow")
# grid[-10:, :, :] = fdtd.PML(name="pml_xhigh")
grid[:, :, 0:10] = fdtd.PML(name="pml_zlow")
grid[:, :, -10:] = fdtd.PML(name="pml_zhigh")
grid[:, 0, :] = fdtd.PeriodicBoundary(name="ybounds")

grid[2:160,2:160,30:31] = fdtd.PlaneSource(
    period=WAVELENGTH / SPEED_LIGHT, name="linesource"
)
grid.run(300)
grid.visualize(z=80,show=True)

Method for calculation of average power

Hi @flaport! For a project I'm working on, I need to calculate the average power (intensity) of light. Ex. simulate imaging it on a detector. Is there a method that you'd recommend for doing that? I've tried using a detector to average the intensity over the last X simulation timesteps, but that so far doesn't seem to give correct results. Thanks!

parallel computation

Hello,
Thanks for your work.
Is there a way to run this code in parallel on cpu?
I assume that it's automatically parallelized if the cuda backend is used.
Is this correct?

Float128 not supported on Windows

I recently installed this on Windows and I'm using Jupyter notebook, It seems to not support the float 128, any thoughts?

AttributeError: module 'numpy' has no attribute 'float128'

Hello! How to obtain the phase of electromagnetic field?

Hello, I found that the calculation results in this project are the amplitude of electromagnetic field, and there is no complex electric field value. What should I do if I want to get the phase value at a certain monitor? thanks!!

Defining conductivity tensor raises error

Hi,

First of all, Thank you for this amazing module!

The issue right now is by defining an absorbing object with a permittivity matrix, perm and a conductivity matrix, cond in a 2D simulation, the code raises an error:

self.grid.E[loc] *= (1 - self.absorption_factor) / (1 + self.absorption_factor)
ValueError: non-broadcastable output operand with shape (64,64,1,3) doesn't match the broadcast shape (64,64,64,3)

perm = np.array([[[item[0]] for item in row] for row in matrix])
cond = np.array([[[item[1]] for item in row] for row in matrix])
grid[10:74, 10:74, 0] = fdtd.AbsorbingObject(permittivity=mask, conductivity = cond, name="object")

Matrix is a 64 by 64 by 2 array containing the permittivity and conductivity at each voxel.

In the code, I'm defining a matrix which will contain the permittivity and conductivity tensor, each with a different value. Removing the conductivity tensor fixes the issue, however and the module works as expected.

Any advice to fix this would be appreciated.

Updating fields

# grid.py
def update_E(self):
        """ update the electric field by using the curl of the magnetic field """

        curl = curl_H(self.H)
        self.E += self.courant_number * self.inverse_permittivity * curl

        # update objects
        for obj in self.objects:
            obj.update_E(curl)

Here you update E for the whole grid. Then you update E inside the objects, changing E in-place, locally.

#objects.py
def update_E(self, curl_H):
        """ custom update equations for inside the anisotropic object
        Args:
            curl_H: the curl of magnetic field in the grid.
        """
        loc = (self.x, self.y, self.z)
        self.grid.E[loc] *= (1 - self.absorption_factor) / (1 + self.absorption_factor)
        self.grid.E[loc] += (
            self.grid.courant_number
            * self.inverse_permittivity
            * curl_H[loc]
            / (1 + self.absorption_factor)
        )

Aren't you doing it twice?

# first time
    E += c*dt*inv(ε)*curl(H)
# second time
    f = 0.5*dt*σ
    E *= inv(1 + f) * (1 - f)
    E += inv(1 + f)*sc*inv(ε)*curl_H

Discontinuity in the curls of E and H??

Hi, firstly thanks for the amazing code.
How do I introduce the discontinuity in the curls of E and H, and in the update equations of E and H?

I tried to introduce the discontinuity in the curl_E and curl_H and update_E and update_H uisng the decorator with out changing the class . it did not work for me. The discontinuity equations I have introduced are (eqn 5 , 6 from supplimentary and eqn 5, 6 from main paper ) from
https://doi.org/10.1038/s41467-018-05579-6 , if you introduce this into your pakage, it will be grate .
I hope we can solve the issue by using decorator on curl_E and curl_H and update_E and updat_H.
thanks for the reply. please let me know your comments.

Pulse

Hey @flaport,
In the class LineSource I can't find the pulse variable which is referred in examples. How to reach this pulse? Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.