forked from jan/fdfd_tools
initial development
This commit is contained in:
parent
282ba3a36e
commit
6c1ceb1670
2
.gitignore
vendored
2
.gitignore
vendored
@ -58,3 +58,5 @@ docs/_build/
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
|
||||
.idea/
|
||||
|
34
README.md
34
README.md
@ -1,3 +1,33 @@
|
||||
# fdfd-tools
|
||||
# fdfd_tools
|
||||
|
||||
Python tools for creating finite-difference frequency-domain (FDFD) electromagnetic simulations.
|
||||
**fdfd_tools** is a python package containing utilities for
|
||||
creating and analyzing 2D and 3D finite-difference frequency-domain (FDFD)
|
||||
electromagnetic simulations.
|
||||
|
||||
|
||||
**Contents**
|
||||
* Library of sparse matrices for representing the electromagnetic wave
|
||||
equation in 3D, as well as auxiliary matrices for conversion between fields
|
||||
* Waveguide mode solver and waveguide mode operators
|
||||
* Stretched-coordinate PML boundaries (SCPML)
|
||||
* Functional versions of most operators
|
||||
* Anisotropic media (eps_xx, eps_yy, eps_zz, mu_xx, ...)
|
||||
|
||||
This package does *not* provide a matrix solver. The waveguide mode solver
|
||||
uses scipy's eigenvalue solver; I recommend a GPU-based iterative solver (eg.
|
||||
those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html)). You will
|
||||
need the ability to solve complex symmetric (non-Hermitian) linear systems,
|
||||
ideally with double precision.
|
||||
|
||||
## Installation
|
||||
|
||||
**Requirements:**
|
||||
* python 3 (written and tested with 3.5)
|
||||
* numpy
|
||||
* scipy
|
||||
|
||||
|
||||
Install with pip, via git:
|
||||
```bash
|
||||
pip install git+https://mpxd.net/gogs/jan/fdfd_tools.git@release
|
||||
```
|
||||
|
234
examples/test.py
Normal file
234
examples/test.py
Normal file
@ -0,0 +1,234 @@
|
||||
import numpy
|
||||
from numpy.ctypeslib import ndpointer
|
||||
import ctypes
|
||||
|
||||
# h5py used by (uncalled) h5_write(); not used in currently-called code
|
||||
|
||||
from fdfd_tools import vec, unvec, waveguide_mode
|
||||
import fdfd_tools, fdfd_tools.functional, fdfd_tools.grid
|
||||
import gridlock
|
||||
|
||||
from matplotlib import pyplot
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
|
||||
|
||||
def complex_to_alternating(x: numpy.ndarray) -> numpy.ndarray:
|
||||
stacked = numpy.vstack((numpy.real(x), numpy.imag(x)))
|
||||
return stacked.T.astype(numpy.float64).flatten()
|
||||
|
||||
|
||||
def solve_A(A, b: numpy.ndarray) -> numpy.ndarray:
|
||||
A_vals = complex_to_alternating(A.data)
|
||||
b_vals = complex_to_alternating(b)
|
||||
x_vals = numpy.zeros_like(b_vals)
|
||||
|
||||
args = ['dummy',
|
||||
'--solver', 'QMR',
|
||||
'--maxiter', '40000',
|
||||
'--atol', '1e-6',
|
||||
'--verbose', '100']
|
||||
argc = ctypes.c_int(len(args))
|
||||
argv_arr_t = ctypes.c_char_p * len(args)
|
||||
argv_arr = argv_arr_t()
|
||||
argv_arr[:] = [s.encode('ascii') for s in args]
|
||||
|
||||
A_dim = ctypes.c_int(A.shape[0])
|
||||
A_nnz = ctypes.c_int(A.nnz)
|
||||
npdouble = ndpointer(ctypes.c_double)
|
||||
npint = ndpointer(ctypes.c_int)
|
||||
|
||||
lib = ctypes.cdll.LoadLibrary('/home/jan/magma_solve/zsolve_shared.so')
|
||||
c_solver = lib.zsolve
|
||||
c_solver.argtypes = [ctypes.c_int, argv_arr_t,
|
||||
ctypes.c_int, ctypes.c_int,
|
||||
npdouble, npint, npint, npdouble, npdouble]
|
||||
|
||||
c_solver(argc, argv_arr, A_dim, A_nnz, A_vals,
|
||||
A.indptr.astype(numpy.intc),
|
||||
A.indices.astype(numpy.intc),
|
||||
b_vals, x_vals)
|
||||
|
||||
x = (x_vals[::2] + 1j * x_vals[1::2]).flatten()
|
||||
return x
|
||||
|
||||
|
||||
def write_h5(filename, A, b):
|
||||
import h5py
|
||||
# dtype=np.dtype([('real', 'float64'), ('imag', 'float64')])
|
||||
h5py.get_config().complex_names = ('real', 'imag')
|
||||
with h5py.File(filename, 'w') as mat_file:
|
||||
mat_file.create_group('/A')
|
||||
mat_file['/A/ir'] = A.indices.astype(numpy.intc)
|
||||
mat_file['/A/jc'] = A.indptr.astype(numpy.intc)
|
||||
mat_file['/A/data'] = A.data
|
||||
mat_file['/b'] = b
|
||||
mat_file['/x'] = numpy.zeros_like(b)
|
||||
|
||||
|
||||
def test0():
|
||||
dx = 50 # discretization (nm/cell)
|
||||
pml_thickness = 10 # (number of cells)
|
||||
|
||||
wl = 1550 # Excitation wavelength
|
||||
omega = 2 * numpy.pi / wl
|
||||
|
||||
# Device design parameters
|
||||
radii = (1, 0.6)
|
||||
th = 220
|
||||
center = [0, 0, 0]
|
||||
|
||||
# refractive indices
|
||||
n_ring = numpy.sqrt(12.6) # ~Si
|
||||
n_air = 4.0 # air
|
||||
|
||||
# Half-dimensions of the simulation grid
|
||||
xyz_max = numpy.array([1.2, 1.2, 0.3]) * 1000 + pml_thickness * dx
|
||||
|
||||
# Coordinates of the edges of the cells.
|
||||
half_edge_coords = [numpy.arange(dx/2, m + dx, step=dx) for m in xyz_max]
|
||||
edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
|
||||
|
||||
# #### Create the grid, mask, and draw the device ####
|
||||
grid = gridlock.Grid(edge_coords, initial=n_air**2, num_grids=3)
|
||||
grid.draw_cylinder(surface_normal=gridlock.Direction.z,
|
||||
center=center,
|
||||
radius=max(radii),
|
||||
thickness=th,
|
||||
eps=n_ring**2,
|
||||
num_points=24)
|
||||
grid.draw_cylinder(surface_normal=gridlock.Direction.z,
|
||||
center=center,
|
||||
radius=min(radii),
|
||||
thickness=th*1.1,
|
||||
eps=n_air ** 2,
|
||||
num_points=24)
|
||||
|
||||
dx0_a = grid.dxyz
|
||||
dx0_b = [grid.shifted_dxyz(which_shifts=a)[a] for a in range(3)]
|
||||
dxes = [dx0_a, dx0_b]
|
||||
for a in (0, 1, 2):
|
||||
for p in (-1, 1):
|
||||
dxes = fdfd_tools.grid.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega,
|
||||
thickness=pml_thickness)
|
||||
|
||||
J = [numpy.zeros_like(grid.grids[0], dtype=complex) for _ in range(3)]
|
||||
J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1e5
|
||||
|
||||
A = fdfd_tools.functional.e_full(omega, dxes, vec(grid.grids)).tocsr()
|
||||
b = -1j * omega * vec(J)
|
||||
|
||||
x = solve_A(A, b)
|
||||
E = unvec(x, grid.shape)
|
||||
|
||||
print('Norm of the residual is {}'.format(numpy.linalg.norm(A.dot(x) - b)/numpy.linalg.norm(b)))
|
||||
|
||||
pyplot.figure()
|
||||
pyplot.pcolor(numpy.real(E[1][:, :, grid.shape[2]//2]), cmap='seismic')
|
||||
pyplot.axis('equal')
|
||||
pyplot.show()
|
||||
|
||||
|
||||
def test1():
|
||||
dx = 40 # discretization (nm/cell)
|
||||
pml_thickness = 10 # (number of cells)
|
||||
|
||||
wl = 1550 # Excitation wavelength
|
||||
omega = 2 * numpy.pi / wl
|
||||
|
||||
# Device design parameters
|
||||
w = 600
|
||||
th = 220
|
||||
center = [0, 0, 0]
|
||||
|
||||
# refractive indices
|
||||
n_wg = numpy.sqrt(12.6) # ~Si
|
||||
n_air = 1.0 # air
|
||||
|
||||
# Half-dimensions of the simulation grid
|
||||
xyz_max = numpy.array([0.8, 0.9, 0.6]) * 1000 + (pml_thickness + 2) * dx
|
||||
|
||||
# Coordinates of the edges of the cells.
|
||||
half_edge_coords = [numpy.arange(dx/2, m + dx/2, step=dx) for m in xyz_max]
|
||||
edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
|
||||
|
||||
# #### Create the grid and draw the device ####
|
||||
grid = gridlock.Grid(edge_coords, initial=n_air**2, num_grids=3)
|
||||
grid.draw_cuboid(center=center, dimensions=[8e3, w, th], eps=n_wg**2)
|
||||
|
||||
dx0_a = grid.dxyz
|
||||
dx0_b = [grid.shifted_dxyz(which_shifts=a)[a] for a in range(3)]
|
||||
dxes = [dx0_a, dx0_b]
|
||||
for a in (0, 1, 2):
|
||||
for p in (-1, 1):
|
||||
dxes = fdfd_tools.grid.stretch_with_scpml(dxes,omega=omega, axis=a, polarity=p,
|
||||
thickness=pml_thickness)
|
||||
|
||||
half_dims = numpy.array([10, 20, 15]) * dx
|
||||
dims = [-half_dims, half_dims]
|
||||
dims[1][0] = dims[0][0]
|
||||
ind_dims = (grid.pos2ind(dims[0], which_shifts=None).astype(int),
|
||||
grid.pos2ind(dims[1], which_shifts=None).astype(int))
|
||||
wg_args = {
|
||||
'omega': omega,
|
||||
'slices': [slice(i, f+1) for i, f in zip(*ind_dims)],
|
||||
'dxes': dxes,
|
||||
'axis': 0,
|
||||
'polarity': +1,
|
||||
}
|
||||
|
||||
wg_results = waveguide_mode.solve_waveguide_mode(mode_number=0, **wg_args, epsilon=grid.grids)
|
||||
J = waveguide_mode.compute_source(**wg_args, **wg_results)
|
||||
H_overlap = waveguide_mode.compute_overlap_e(**wg_args, **wg_results)
|
||||
|
||||
A = fdfd_tools.operators.e_full(omega, dxes, vec(grid.grids)).tocsr()
|
||||
b = -1j * omega * vec(J)
|
||||
x = solve_A(A, b)
|
||||
E = unvec(x, grid.shape)
|
||||
|
||||
print('Norm of the residual is ', numpy.linalg.norm(A @ x - b))
|
||||
|
||||
def pcolor(v):
|
||||
vmax = numpy.max(numpy.abs(v))
|
||||
pyplot.pcolor(v, cmap='seismic', vmin=-vmax, vmax=vmax)
|
||||
pyplot.axis('equal')
|
||||
pyplot.colorbar()
|
||||
|
||||
center = grid.pos2ind([0, 0, 0], None).astype(int)
|
||||
pyplot.figure()
|
||||
pyplot.subplot(2, 2, 1)
|
||||
pcolor(numpy.real(E[1][center[0], :, :]))
|
||||
pyplot.subplot(2, 2, 2)
|
||||
pyplot.plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10))
|
||||
pyplot.subplot(2, 2, 3)
|
||||
pcolor(numpy.real(E[1][:, :, center[2]]))
|
||||
pyplot.subplot(2, 2, 4)
|
||||
|
||||
def poyntings(E):
|
||||
e = vec(E)
|
||||
h = fdfd_tools.operators.e2h(omega, dxes) @ e
|
||||
cross1 = fdfd_tools.operators.poynting_e_cross(e, dxes) @ h.conj()
|
||||
cross2 = fdfd_tools.operators.poynting_h_cross(h.conj(), dxes) @ e
|
||||
s1 = unvec(0.5 * numpy.real(cross1), grid.shape)
|
||||
s2 = unvec(0.5 * numpy.real(-cross2), grid.shape)
|
||||
return s1, s2
|
||||
|
||||
s1x, s2x = poyntings(E)
|
||||
pyplot.plot(s1x[0].sum(axis=2).sum(axis=1))
|
||||
pyplot.hold(True)
|
||||
pyplot.plot(s2x[0].sum(axis=2).sum(axis=1))
|
||||
pyplot.show()
|
||||
|
||||
q = []
|
||||
for i in range(-5, 30):
|
||||
H_rolled = [numpy.roll(h, i, axis=0) for h in H_overlap]
|
||||
q += [numpy.abs(vec(E) @ vec(H_rolled))]
|
||||
pyplot.figure()
|
||||
pyplot.plot(q)
|
||||
pyplot.title('Overlap with mode')
|
||||
pyplot.show()
|
||||
print('Average overlap with mode:', sum(q)/len(q))
|
||||
|
||||
if __name__ == '__main__':
|
||||
# test0()
|
||||
test1()
|
25
fdfd_tools/__init__.py
Normal file
25
fdfd_tools/__init__.py
Normal file
@ -0,0 +1,25 @@
|
||||
"""
|
||||
Electromagnetic FDFD simulation tools
|
||||
|
||||
Tools for 3D and 2D Electromagnetic Finite Difference Frequency Domain (FDFD)
|
||||
simulations. These tools handle conversion of fields to/from vector form,
|
||||
creation of the wave operator matrix, stretched-coordinate PMLs,
|
||||
field conversion operators, waveguide mode operator, and waveguide mode
|
||||
solver.
|
||||
|
||||
This package only contains a solver for the waveguide mode eigenproblem;
|
||||
if you want to solve 3D problems you can use your favorite iterative sparse
|
||||
matrix solver (so long as it can handle complex symmetric [non-Hermitian]
|
||||
matrices, ideally with double precision).
|
||||
|
||||
|
||||
Dependencies:
|
||||
- numpy
|
||||
- scipy
|
||||
|
||||
"""
|
||||
|
||||
from .vectorization import vec, unvec, field_t, vfield_t
|
||||
from .grid import dx_lists_t
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
149
fdfd_tools/functional.py
Normal file
149
fdfd_tools/functional.py
Normal file
@ -0,0 +1,149 @@
|
||||
"""
|
||||
Functional versions of many FDFD operators. These can be useful for performing
|
||||
FDFD calculations without needing to construct large matrices in memory.
|
||||
|
||||
The functions generated here expect inputs in the form E = [E_x, E_y, E_z], where each
|
||||
component E_* is an ndarray of equal shape.
|
||||
"""
|
||||
from typing import List, Callable
|
||||
import numpy
|
||||
|
||||
from . import dx_lists_t, field_t
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
|
||||
|
||||
functional_matrix = Callable[[List[numpy.ndarray]], List[numpy.ndarray]]
|
||||
|
||||
|
||||
def curl_h(dxes: dx_lists_t) -> functional_matrix:
|
||||
"""
|
||||
Curl operator for use with the H field.
|
||||
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:return: Function for taking the discretized curl of the H-field, F(H) -> curlH
|
||||
"""
|
||||
dxyz_b = numpy.meshgrid(*dxes[1], indexing='ij')
|
||||
|
||||
def dH(f, ax):
|
||||
return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax]
|
||||
|
||||
def ch_fun(H: List[numpy.ndarray]) -> List[numpy.ndarray]:
|
||||
E = [dH(H[2], 1) - dH(H[1], 2),
|
||||
dH(H[0], 2) - dH(H[2], 0),
|
||||
dH(H[1], 0) - dH(H[0], 1)]
|
||||
return E
|
||||
|
||||
return ch_fun
|
||||
|
||||
|
||||
def curl_e(dxes: dx_lists_t) -> functional_matrix:
|
||||
"""
|
||||
Curl operator for use with the E field.
|
||||
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:return: Function for taking the discretized curl of the E-field, F(E) -> curlE
|
||||
"""
|
||||
dxyz_a = numpy.meshgrid(*dxes[0], indexing='ij')
|
||||
|
||||
def dE(f, ax):
|
||||
return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax]
|
||||
|
||||
def ce_fun(E: List[numpy.ndarray]) -> List[numpy.ndarray]:
|
||||
H = [dE(E[2], 1) - dE(E[1], 2),
|
||||
dE(E[0], 2) - dE(E[2], 0),
|
||||
dE(E[1], 0) - dE(E[0], 1)]
|
||||
return H
|
||||
|
||||
return ce_fun
|
||||
|
||||
|
||||
def e_full(omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
epsilon: field_t,
|
||||
mu: field_t = None
|
||||
) -> functional_matrix:
|
||||
"""
|
||||
Wave operator del x (1/mu * del x) - omega**2 * epsilon, for use with E-field,
|
||||
with wave equation
|
||||
(del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J
|
||||
|
||||
:param omega: Angular frequency of the simulation
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:param epsilon: Dielectric constant
|
||||
:param mu: Magnetic permeability (default 1 everywhere)
|
||||
:return: Function implementing the wave operator A(E) -> E
|
||||
"""
|
||||
ch = curl_h(dxes)
|
||||
ce = curl_e(dxes)
|
||||
|
||||
def op_1(E):
|
||||
curls = ch(ce(E))
|
||||
return [c - omega ** 2 * e * x for c, e, x in zip(curls, epsilon, E)]
|
||||
|
||||
def op_mu(E):
|
||||
curls = ch([m * y for m, y in zip(mu, ce(E))])
|
||||
return [c - omega ** 2 * e * x for c, e, x in zip(curls, epsilon, E)]
|
||||
|
||||
if numpy.any(numpy.equal(mu, None)):
|
||||
return op_1
|
||||
else:
|
||||
return op_mu
|
||||
|
||||
|
||||
def eh_full(omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
epsilon: field_t,
|
||||
mu: field_t = None
|
||||
) -> functional_matrix:
|
||||
"""
|
||||
Wave operator for full (both E and H) field representation.
|
||||
|
||||
:param omega: Angular frequency of the simulation
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:param epsilon: Dielectric constant
|
||||
:param mu: Magnetic permeability (default 1 everywhere)
|
||||
:return: Function implementing the wave operator A(E, H) -> (E, H)
|
||||
"""
|
||||
ch = curl_h(dxes)
|
||||
ce = curl_e(dxes)
|
||||
|
||||
def op_1(E, H):
|
||||
return ([c - 1j * omega * e * x for c, e, x in zip(ch(H), epsilon, E)],
|
||||
[c + 1j * omega * y for c, y in zip(ce(E), H)])
|
||||
|
||||
def op_mu(E, H):
|
||||
return ([c - 1j * omega * e * x for c, e, x in zip(ch(H), epsilon, E)],
|
||||
[c + 1j * omega * m * y for c, m, y in zip(ce(E), mu, H)])
|
||||
|
||||
if numpy.any(numpy.equal(mu, None)):
|
||||
return op_1
|
||||
else:
|
||||
return op_mu
|
||||
|
||||
|
||||
def e2h(omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
mu: field_t = None,
|
||||
) -> functional_matrix:
|
||||
"""
|
||||
Utility operator for converting the E field into the H field.
|
||||
For use with e_full -- assumes that there is no magnetic current M.
|
||||
|
||||
:param omega: Angular frequency of the simulation
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:param mu: Magnetic permeability (default 1 everywhere)
|
||||
:return: Function for converting E to H
|
||||
"""
|
||||
A2 = curl_e(dxes)
|
||||
|
||||
def e2h_1_1(E):
|
||||
return [y / (-1j * omega) for y in A2(E)]
|
||||
|
||||
def e2h_mu(E):
|
||||
return [y / (-1j * omega * m) for y, m in zip(A2(E), mu)]
|
||||
|
||||
if numpy.any(numpy.equal(mu, None)):
|
||||
return e2h_1_1
|
||||
else:
|
||||
return e2h_mu
|
167
fdfd_tools/grid.py
Normal file
167
fdfd_tools/grid.py
Normal file
@ -0,0 +1,167 @@
|
||||
"""
|
||||
Functions for creating stretched coordinate PMLs.
|
||||
"""
|
||||
|
||||
from typing import List, Tuple, Callable
|
||||
import numpy
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
|
||||
|
||||
dx_lists_t = List[List[numpy.ndarray]]
|
||||
s_function_type = Callable[[float], float]
|
||||
|
||||
|
||||
def prepare_s_function(ln_R: float = -16,
|
||||
m: float = 4
|
||||
) -> s_function_type:
|
||||
"""
|
||||
Create an s_function to pass to the SCPML functions. This is used when you would like to
|
||||
customize the PML parameters.
|
||||
|
||||
:param ln_R: Natural logarithm of the desired reflectance
|
||||
:param m: Polynomial order for the PML (imaginary part increases as distance ** m)
|
||||
:return: An s_function, which takes an ndarray (distances) and returns an ndarray (complex part of
|
||||
the cell width; needs to be divided by sqrt(epilon_effective) * real(omega)) before
|
||||
use.
|
||||
"""
|
||||
def s_factor(distance: numpy.ndarray) -> numpy.ndarray:
|
||||
s_max = (m + 1) * ln_R / 2 # / 2 because we have assume boundaries
|
||||
return s_max * (distance ** m)
|
||||
return s_factor
|
||||
|
||||
|
||||
def uniform_grid_scpml(shape: numpy.ndarray or List[int],
|
||||
thicknesses: numpy.ndarray or List[int],
|
||||
omega: float,
|
||||
epsilon_effective: float = 1.0,
|
||||
s_function: s_function_type = None,
|
||||
) -> dx_lists_t:
|
||||
"""
|
||||
Create dx arrays for a uniform grid with a cell width of 1 and a pml.
|
||||
|
||||
If you want something more fine-grained, check out stretch_with_scpml(...).
|
||||
|
||||
:param shape: Shape of the grid, including the PMLs (which are 2*thicknesses thick)
|
||||
:param thicknesses: [th_x, th_y, th_z] Thickness of the PML in each direction. Both polarities are added.
|
||||
Each th_ of pml is applied twice, once on each edge of the grid along the given axis.
|
||||
th_* may be zero, in which case no pml is added.
|
||||
:param omega: Angular frequency for the simulation
|
||||
:param epsilon_effective: Effective epsilon of the PML. Match this to the material at the edge of your grid.
|
||||
Default 1.
|
||||
:param s_function: s_function created by prepare_s_function(...), allowing customization of pml parameters.
|
||||
Default uses prepare_s_function() with no parameters.
|
||||
:return: Complex cell widths (dx_lists)
|
||||
"""
|
||||
if s_function is None:
|
||||
s_function = prepare_s_function()
|
||||
|
||||
# Normalized distance to nearest boundary
|
||||
def l(u, n, t):
|
||||
return ((t - u).clip(0) + (u - (n - t)).clip(0)) / t
|
||||
|
||||
dx_a = [numpy.array(numpy.inf)] * 3
|
||||
dx_b = [numpy.array(numpy.inf)] * 3
|
||||
|
||||
# divide by this to adjust for epsilon_effective and omega
|
||||
s_correction = numpy.sqrt(epsilon_effective) * numpy.real(omega)
|
||||
|
||||
for k, th in enumerate(thicknesses):
|
||||
s = shape[k]
|
||||
if th > 0:
|
||||
sr = numpy.arange(s)
|
||||
dx_a[k] = 1 + 1j * s_function(l(sr, s, th)) / s_correction
|
||||
dx_b[k] = 1 + 1j * s_function(l(sr+0.5, s, th)) / s_correction
|
||||
else:
|
||||
dx_a[k] = numpy.ones((s,))
|
||||
dx_b[k] = numpy.ones((s,))
|
||||
return [dx_a, dx_b]
|
||||
|
||||
|
||||
def stretch_with_scpml(dxes: dx_lists_t,
|
||||
axis: int,
|
||||
polarity: int,
|
||||
omega: float,
|
||||
epsilon_effective: float = 1.0,
|
||||
thickness: int = 10,
|
||||
s_function: s_function_type = None,
|
||||
) -> dx_lists_t:
|
||||
"""
|
||||
Stretch dxes to contain a stretched-coordinate PML (SCPML) in one direction along one axis.
|
||||
|
||||
:param dxes: dx_tuple with coordinates to stretch
|
||||
:param axis: axis to stretch (0=x, 1=y, 2=z)
|
||||
:param polarity: direction to stretch (-1 for -ve, +1 for +ve)
|
||||
:param omega: Angular frequency for the simulation
|
||||
:param epsilon_effective: Effective epsilon of the PML. Match this to the material at the edge of your grid.
|
||||
Default 1.
|
||||
:param thickness: number of cells to use for pml (default 10)
|
||||
:param s_function: s_function created by prepare_s_function(...), allowing customization of pml parameters.
|
||||
Default uses prepare_s_function() with no parameters.
|
||||
:return: Complex cell widths
|
||||
"""
|
||||
if s_function is None:
|
||||
s_function = prepare_s_function()
|
||||
|
||||
dx_ai = dxes[0][axis].astype(complex)
|
||||
dx_bi = dxes[1][axis].astype(complex)
|
||||
|
||||
pos = numpy.hstack((0, dx_ai.cumsum()))
|
||||
pos_a = (pos[:-1] + pos[1:]) / 2
|
||||
pos_b = pos[:-1]
|
||||
|
||||
# divide by this to adjust for epsilon_effective and omega
|
||||
s_correction = numpy.sqrt(epsilon_effective) * numpy.real(omega)
|
||||
|
||||
if polarity > 0:
|
||||
# front pml
|
||||
bound = pos[thickness]
|
||||
d = bound - pos[0]
|
||||
|
||||
def l_d(x):
|
||||
return (bound - x) / (bound - pos[0])
|
||||
|
||||
slc = slice(thickness)
|
||||
|
||||
else:
|
||||
# back pml
|
||||
bound = pos[-thickness - 1]
|
||||
d = pos[-1] - bound
|
||||
|
||||
def l_d(x):
|
||||
return (x - bound) / (pos[-1] - bound)
|
||||
|
||||
if thickness == 0:
|
||||
slc = slice(None)
|
||||
else:
|
||||
slc = slice(-thickness, None)
|
||||
|
||||
dx_ai[slc] *= 1 + 1j * s_function(l_d(pos_a[slc])) / d / s_correction
|
||||
dx_bi[slc] *= 1 + 1j * s_function(l_d(pos_b[slc])) / d / s_correction
|
||||
|
||||
dxes[0][axis] = dx_ai
|
||||
dxes[1][axis] = dx_bi
|
||||
|
||||
return dxes
|
||||
|
||||
|
||||
def generate_dx(pos: List[numpy.ndarray]) -> dx_lists_t:
|
||||
"""
|
||||
Given a list of 3 ndarrays cell centers, creates the cell width parameters.
|
||||
|
||||
:param pos: List of 3 ndarrays of cell centers
|
||||
:return: (dx_a, dx_b) cell widths (no pml)
|
||||
"""
|
||||
if len(pos) != 3:
|
||||
raise Exception('Must have len(pos) == 3')
|
||||
|
||||
dx_a = [numpy.array(numpy.inf)] * 3
|
||||
dx_b = [numpy.array(numpy.inf)] * 3
|
||||
|
||||
for i, p_orig in enumerate(pos):
|
||||
p = numpy.array(p_orig, dtype=float)
|
||||
if p.size != 1:
|
||||
p_shifted = numpy.hstack((p[1:], p[-1] + (p[1] - p[0])))
|
||||
dx_a[i] = numpy.diff(p)
|
||||
dx_b[i] = numpy.diff((p + p_shifted) / 2)
|
||||
return dx_a, dx_b
|
397
fdfd_tools/operators.py
Normal file
397
fdfd_tools/operators.py
Normal file
@ -0,0 +1,397 @@
|
||||
"""
|
||||
Sparse matrix operators for use with electromagnetic wave equations.
|
||||
|
||||
These functions return sparse-matrix (scipy.sparse.spmatrix) representations of
|
||||
a variety of operators, intended for use with E and H fields vectorized using the
|
||||
fdfd_tools.vec() and .unvec() functions (column-major/Fortran ordering).
|
||||
|
||||
E- and H-field values are defined on a Yee cell; epsilon values should be calculated for
|
||||
cells centered at each E component (mu at each H component).
|
||||
|
||||
Many of these functions require a 'dxes' parameter, of type fdfd_tools.dx_lists_type,
|
||||
which contains grid cell width information in the following format:
|
||||
[[[dx_e_0, dx_e_1, ...], [dy_e_0, ...], [dz_e_0, ...]],
|
||||
[[dx_h_0, dx_h_1, ...], [dy_h_0, ...], [dz_h_0, ...]]]
|
||||
where dx_e_0 is the x-width of the x=0 cells, as used when calculating dE/dx,
|
||||
and dy_h_0 is the y-width of the y=0 cells, as used when calculating dH/dy, etc.
|
||||
|
||||
|
||||
The following operators are included:
|
||||
- E-only wave operator
|
||||
- H-only wave operator
|
||||
- EH wave operator
|
||||
- Curl for use with E, H fields
|
||||
- E to H conversion
|
||||
- M to J conversion
|
||||
- Poynting cross products
|
||||
|
||||
Also available:
|
||||
- Circular shifts
|
||||
- Discrete derivatives
|
||||
- Averaging operators
|
||||
- Cross product matrices
|
||||
"""
|
||||
|
||||
from typing import List, Tuple
|
||||
import numpy
|
||||
import scipy.sparse as sparse
|
||||
|
||||
from . import vec, dx_lists_t, vfield_t
|
||||
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
|
||||
|
||||
def e_full(omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
epsilon: vfield_t,
|
||||
mu: vfield_t = None
|
||||
) -> sparse.spmatrix:
|
||||
"""
|
||||
Wave operator del x (1/mu * del x) - omega**2 * epsilon, for use with E-field,
|
||||
with wave equation
|
||||
(del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J
|
||||
|
||||
To make this matrix symmetric, use the preconditions from e_full_preconditioners().
|
||||
|
||||
:param omega: Angular frequency of the simulation
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:param epsilon: Vectorized dielectric constant
|
||||
:param mu: Vectorized magnetic permeability (default 1 everywhere)..
|
||||
:return: Sparse matrix containing the wave operator
|
||||
"""
|
||||
ce = curl_e(dxes)
|
||||
ch = curl_h(dxes)
|
||||
|
||||
e = sparse.diags(epsilon)
|
||||
if numpy.any(numpy.equal(mu, None)):
|
||||
m_div = sparse.eye(epsilon.size)
|
||||
else:
|
||||
m_div = sparse.diags(1 / mu)
|
||||
|
||||
op = ch @ m_div @ ce - omega**2 * e
|
||||
return op
|
||||
|
||||
|
||||
def e_full_preconditioners(dxes: dx_lists_t
|
||||
) -> Tuple[sparse.spmatrix, sparse.spmatrix]:
|
||||
"""
|
||||
Left and right preconditioners (Pl, Pr) for symmetrizing the e_full wave operator.
|
||||
|
||||
The preconditioned matrix A_symm = (Pl @ A @ Pr) is complex-symmetric
|
||||
(non-Hermitian unless there is no loss or PMLs).
|
||||
|
||||
The preconditioner matrices are diagonal and complex, with Pr = 1 / Pl
|
||||
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:return: Preconditioner matrices (Pl, Pr)
|
||||
"""
|
||||
p_squared = [dxes[0][0][:, None, None] * dxes[1][1][None, :, None] * dxes[1][2][None, None, :],
|
||||
dxes[1][0][:, None, None] * dxes[0][1][None, :, None] * dxes[1][2][None, None, :],
|
||||
dxes[1][0][:, None, None] * dxes[1][1][None, :, None] * dxes[0][2][None, None, :]]
|
||||
|
||||
p_vector = numpy.sqrt(vec(p_squared))
|
||||
P_left = sparse.diags(p_vector)
|
||||
P_right = sparse.diags(1 / p_vector)
|
||||
return P_left, P_right
|
||||
|
||||
|
||||
def h_full(omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
epsilon: vfield_t,
|
||||
mu: vfield_t = None
|
||||
) -> sparse.spmatrix:
|
||||
"""
|
||||
Wave operator del x (1/epsilon * del x) - omega**2 * mu, for use with H-field,
|
||||
with wave equation
|
||||
(del x (1/epsilon * del x) - omega**2 * mu) H = i * omega * M
|
||||
|
||||
:param omega: Angular frequency of the simulation
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:param epsilon: Vectorized dielectric constant
|
||||
:param mu: Vectorized magnetic permeability (default 1 everywhere)
|
||||
:return: Sparse matrix containing the wave operator
|
||||
"""
|
||||
ec = curl_e(dxes)
|
||||
hc = curl_h(dxes)
|
||||
|
||||
e_div = sparse.diags(1 / epsilon)
|
||||
if numpy.any(numpy.equal(mu, None)):
|
||||
m = sparse.eye(epsilon.size)
|
||||
else:
|
||||
m = sparse.diags(mu)
|
||||
|
||||
A = ec @ e_div @ hc - omega**2 * m
|
||||
return A
|
||||
|
||||
|
||||
def eh_full(omega, dxes, epsilon, mu=None):
|
||||
"""
|
||||
Wave operator for [E, H] field representation. This operator implements Maxwell's
|
||||
equations without cancelling out either E or H. The operator is
|
||||
[[-i * omega * epsilon, del x],
|
||||
[del x, i * omega * mu]]
|
||||
|
||||
for use with a field vector of the form hstack(vec(E), vec(H)).
|
||||
|
||||
:param omega: Angular frequency of the simulation
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:param epsilon: Vectorized dielectric constant
|
||||
:param mu: Vectorized magnetic permeability (default 1 everywhere)
|
||||
:return: Sparse matrix containing the wave operator
|
||||
"""
|
||||
A2 = curl_e(dxes)
|
||||
A1 = curl_h(dxes)
|
||||
|
||||
iwe = 1j * omega * sparse.diags(epsilon)
|
||||
iwm = 1j * omega
|
||||
if not numpy.any(numpy.equal(mu, None)):
|
||||
iwm *= sparse.diags(mu)
|
||||
|
||||
A = sparse.bmat([[-iwe, A1],
|
||||
[A2, +iwm]])
|
||||
return A
|
||||
|
||||
|
||||
def curl_h(dxes: dx_lists_t) -> sparse.spmatrix:
|
||||
"""
|
||||
Curl operator for use with the H field.
|
||||
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:return: Sparse matrix for taking the discretized curl of the H-field
|
||||
"""
|
||||
return cross(deriv_back(dxes[1]))
|
||||
|
||||
|
||||
def curl_e(dxes: dx_lists_t) -> sparse.spmatrix:
|
||||
"""
|
||||
Curl operator for use with the E field.
|
||||
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:return: Sparse matrix for taking the discretized curl of the E-field
|
||||
"""
|
||||
return cross(deriv_forward(dxes[0]))
|
||||
|
||||
|
||||
def e2h(omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
mu: vfield_t = None,
|
||||
) -> sparse.spmatrix:
|
||||
"""
|
||||
Utility operator for converting the E field into the H field.
|
||||
For use with e_full -- assumes that there is no magnetic current M.
|
||||
|
||||
:param omega: Angular frequency of the simulation
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:param mu: Vectorized magnetic permeability (default 1 everywhere)
|
||||
:return: Sparse matrix for converting E to H
|
||||
"""
|
||||
op = curl_e(dxes) / (-1j * omega)
|
||||
|
||||
if not numpy.any(numpy.equal(mu, None)):
|
||||
op = sparse.diags(1 / mu) @ op
|
||||
|
||||
return op
|
||||
|
||||
|
||||
def m2j(omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
mu: vfield_t = None):
|
||||
"""
|
||||
Utility operator for converting M field into J.
|
||||
Converts a magnetic current M into an electric current J.
|
||||
For use with eg. e_full.
|
||||
|
||||
:param omega: Angular frequency of the simulation
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:param mu: Vectorized magnetic permeability (default 1 everywhere)
|
||||
:return: Sparse matrix for converting E to H
|
||||
"""
|
||||
op = curl_h(dxes) / (1j * omega)
|
||||
|
||||
if not numpy.any(numpy.equal(mu, None)):
|
||||
op = op @ sparse.diags(1 / mu)
|
||||
|
||||
return op
|
||||
|
||||
|
||||
def rotation(axis: int, shape: List[int]) -> sparse.spmatrix:
|
||||
"""
|
||||
Utility operator for performing a circular shift along a specified axis by 1 element.
|
||||
|
||||
:param axis: Axis to shift along. x=0, y=1, z=2
|
||||
:param shape: Shape of the grid being shifted
|
||||
:return: Sparse matrix for performing the circular shift
|
||||
"""
|
||||
if len(shape) not in (2, 3):
|
||||
raise Exception('Invalid shape: {}'.format(shape))
|
||||
if axis not in range(len(shape)):
|
||||
raise Exception('Invalid direction: {}, shape is {}'.format(axis, shape))
|
||||
|
||||
n = numpy.prod(shape)
|
||||
|
||||
shifts = [1 if k == axis else 0 for k in range(3)]
|
||||
shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts)]
|
||||
ijk = numpy.meshgrid(*shifted_diags, indexing='ij')
|
||||
|
||||
i_ind = numpy.arange(n)
|
||||
j_ind = ijk[0] + ijk[1] * shape[0]
|
||||
if len(shape) == 3:
|
||||
j_ind += ijk[2] * shape[0] * shape[1]
|
||||
|
||||
vij = (numpy.ones(n), (i_ind, j_ind.flatten(order='F')))
|
||||
|
||||
D = sparse.csr_matrix(vij, shape=(n, n))
|
||||
return D
|
||||
|
||||
|
||||
def deriv_forward(dx_e: List[numpy.ndarray]) -> List[sparse.spmatrix]:
|
||||
"""
|
||||
Utility operators for taking discretized derivatives (forward variant).
|
||||
|
||||
:param dx_e: Lists of cell sizes for all axes [[dx_0, dx_1, ...], ...].
|
||||
:return: List of operators for taking forward derivatives along each axis.
|
||||
"""
|
||||
shape = [s.size for s in dx_e]
|
||||
n = numpy.prod(shape)
|
||||
|
||||
dx_e_expanded = numpy.meshgrid(*dx_e, indexing='ij')
|
||||
|
||||
def deriv(axis):
|
||||
return rotation(axis, shape) - sparse.eye(n)
|
||||
|
||||
Ds = [sparse.diags(+1 / dx.flatten(order='F')) @ deriv(a)
|
||||
for a, dx in enumerate(dx_e_expanded)]
|
||||
|
||||
return Ds
|
||||
|
||||
|
||||
def deriv_back(dx_h: List[numpy.ndarray]) -> List[sparse.spmatrix]:
|
||||
"""
|
||||
Utility operators for taking discretized derivatives (backward variant).
|
||||
|
||||
:param dx_h: Lists of cell sizes for all axes [[dx_0, dx_1, ...], ...].
|
||||
:return: List of operators for taking forward derivatives along each axis.
|
||||
"""
|
||||
shape = [s.size for s in dx_h]
|
||||
n = numpy.prod(shape)
|
||||
|
||||
dx_h_expanded = numpy.meshgrid(*dx_h, indexing='ij')
|
||||
|
||||
def deriv(axis):
|
||||
return rotation(axis, shape) - sparse.eye(n)
|
||||
|
||||
Ds = [sparse.diags(-1 / dx.flatten(order='F')) @ deriv(a).T
|
||||
for a, dx in enumerate(dx_h_expanded)]
|
||||
|
||||
return Ds
|
||||
|
||||
|
||||
def cross(B: List[sparse.spmatrix]) -> sparse.spmatrix:
|
||||
"""
|
||||
Cross product operator
|
||||
|
||||
:param B: List [Bx, By, Bz] of sparse matrices corresponding to the x, y, z
|
||||
portions of the operator on the left side of the cross product.
|
||||
:return: Sparse matrix corresponding to (B x), where x is the cross product
|
||||
"""
|
||||
n = B[0].shape[0]
|
||||
zero = sparse.csr_matrix((n, n))
|
||||
return sparse.bmat([[zero, -B[2], B[1]],
|
||||
[B[2], zero, -B[0]],
|
||||
[-B[1], B[0], zero]])
|
||||
|
||||
|
||||
def vec_cross(b: vfield_t) -> sparse.spmatrix:
|
||||
"""
|
||||
Vector cross product operator
|
||||
|
||||
:param b: Vector on the left side of the cross product
|
||||
:return: Sparse matrix corresponding to (b x), where x is the cross product
|
||||
"""
|
||||
B = [sparse.diags(c) for c in numpy.split(b, 3)]
|
||||
return cross(B)
|
||||
|
||||
|
||||
def avgf(axis: int, shape: List[int]) -> sparse.spmatrix:
|
||||
"""
|
||||
Forward average operator (x4 = (x4 + x5) / 2)
|
||||
|
||||
:param axis: Axis to average along (x=0, y=1, z=2)
|
||||
:param shape: Shape of the grid to average
|
||||
:return: Sparse matrix for forward average operation
|
||||
"""
|
||||
if len(shape) not in (2, 3):
|
||||
raise Exception('Invalid shape: {}'.format(shape))
|
||||
|
||||
n = numpy.prod(shape)
|
||||
return 0.5 * (sparse.eye(n) + rotation(axis, shape))
|
||||
|
||||
|
||||
def avgb(axis: int, shape: List[int]) -> sparse.spmatrix:
|
||||
"""
|
||||
Backward average operator (x4 = (x4 + x3) / 2)
|
||||
|
||||
:param axis: Axis to average along (x=0, y=1, z=2)
|
||||
:param shape: Shape of the grid to average
|
||||
:return: Sparse matrix for backward average operation
|
||||
"""
|
||||
return avgf(axis, shape).T
|
||||
|
||||
|
||||
def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
|
||||
"""
|
||||
Operator for computing the Poynting vector, contining the (E x) portion of the Poynting vector.
|
||||
|
||||
:param e: Vectorized E-field for the ExH cross product
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:return: Sparse matrix containing (E x) portion of Poynting cross product
|
||||
"""
|
||||
shape = [len(dx) for dx in dxes[0]]
|
||||
|
||||
fx, fy, fz = [avgf(i, shape) for i in range(3)]
|
||||
bx, by, bz = [avgb(i, shape) for i in range(3)]
|
||||
|
||||
dxag = [dx.flatten(order='F') for dx in numpy.meshgrid(*dxes[0], indexing='ij')]
|
||||
dbgx, dbgy, dbgz = [sparse.diags(dx.flatten(order='F'))
|
||||
for dx in numpy.meshgrid(*dxes[1], indexing='ij')]
|
||||
|
||||
Ex, Ey, Ez = [sparse.diags(ei * da) for ei, da in zip(numpy.split(e, 3), dxag)]
|
||||
|
||||
n = numpy.prod(shape)
|
||||
zero = sparse.csr_matrix((n, n))
|
||||
|
||||
P = sparse.bmat(
|
||||
[[ zero, -fx @ Ez @ bz @ dbgy, fx @ Ey @ by @ dbgz],
|
||||
[ fy @ Ez @ bz @ dbgx, zero, -fy @ Ex @ bx @ dbgz],
|
||||
[-fz @ Ey @ by @ dbgx, fz @ Ex @ bx @ dbgy, zero]])
|
||||
return P
|
||||
|
||||
|
||||
def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
|
||||
"""
|
||||
Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector.
|
||||
|
||||
:param h: Vectorized H-field for the HxE cross product
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:return: Sparse matrix containing (H x) portion of Poynting cross product
|
||||
"""
|
||||
shape = [len(dx) for dx in dxes[0]]
|
||||
|
||||
fx, fy, fz = [avgf(i, shape) for i in range(3)]
|
||||
bx, by, bz = [avgb(i, shape) for i in range(3)]
|
||||
|
||||
dxbg = [dx.flatten(order='F') for dx in numpy.meshgrid(*dxes[1], indexing='ij')]
|
||||
dagx, dagy, dagz = [sparse.diags(dx.flatten(order='F'))
|
||||
for dx in numpy.meshgrid(*dxes[0], indexing='ij')]
|
||||
|
||||
Hx, Hy, Hz = [sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg)]
|
||||
|
||||
n = numpy.prod(shape)
|
||||
zero = sparse.csr_matrix((n, n))
|
||||
|
||||
P = sparse.bmat(
|
||||
[[ zero, -by @ Hz @ fx @ dagy, bz @ Hy @ fx @ dagz],
|
||||
[ bx @ Hz @ fy @ dagx, zero, -bz @ Hx @ fy @ dagz],
|
||||
[-bx @ Hy @ fz @ dagx, by @ Hx @ fz @ dagy, zero]])
|
||||
return P
|
49
fdfd_tools/vectorization.py
Normal file
49
fdfd_tools/vectorization.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""
|
||||
Functions for moving between a vector field (list of 3 ndarrays, [f_x, f_y, f_z])
|
||||
and a 1D array representation of that field [f_x0, f_x1, f_x2,... f_y0,... f_z0,...].
|
||||
Vectorized versions of the field use column-major (ie., Fortran, Matlab) ordering.
|
||||
"""
|
||||
|
||||
|
||||
from typing import List
|
||||
import numpy
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
|
||||
# Types
|
||||
field_t = List[numpy.ndarray] # vector field (eg. [E_x, E_y, E_z]
|
||||
vfield_t = numpy.ndarray # linearized vector field
|
||||
|
||||
|
||||
def vec(f: field_t) -> vfield_t:
|
||||
"""
|
||||
Create a 1D ndarray from a 3D vector field which spans a 1-3D region.
|
||||
|
||||
Returns None if called with f=None.
|
||||
|
||||
:param f: A vector field, [f_x, f_y, f_z] where each f_ component is a 1 to
|
||||
3D ndarray (f_* should all be the same size). Doesn't fail with f=None.
|
||||
:return: A 1D ndarray containing the linearized field (or None)
|
||||
"""
|
||||
if numpy.any(numpy.equal(f, None)):
|
||||
return None
|
||||
return numpy.hstack(tuple((fi.flatten(order='F') for fi in f)))
|
||||
|
||||
|
||||
def unvec(v: vfield_t, shape: numpy.ndarray) -> field_t:
|
||||
"""
|
||||
Perform the inverse of vec(): take a 1D ndarray and output a 3D field
|
||||
of form [f_x, f_y, f_z] where each of f_* is a len(shape)-dimensional
|
||||
ndarray.
|
||||
|
||||
Returns None if called with v=None.
|
||||
|
||||
:param v: 1D ndarray representing a 3D vector field of shape shape (or None)
|
||||
:param shape: shape of the vector field
|
||||
:return: [f_x, f_y, f_z] where each f_ is a len(shape) dimensional ndarray
|
||||
(or None)
|
||||
"""
|
||||
if numpy.any(numpy.equal(v, None)):
|
||||
return None
|
||||
return [vi.reshape(shape, order='F') for vi in numpy.split(v, 3)]
|
||||
|
309
fdfd_tools/waveguide.py
Normal file
309
fdfd_tools/waveguide.py
Normal file
@ -0,0 +1,309 @@
|
||||
"""
|
||||
Various operators and helper functions for solving for waveguide modes.
|
||||
|
||||
Assuming a z-dependence of the from exp(-i * wavenumber * z), we can simplify Maxwell's
|
||||
equations in the absence of sources to the form
|
||||
|
||||
A @ [H_x, H_y] = wavenumber**2 * [H_x, H_y]
|
||||
|
||||
with A =
|
||||
omega**2 * epsilon * mu +
|
||||
epsilon * [[-Dy], [Dx]] / epsilon * [-Dy, Dx] +
|
||||
[[Dx], [Dy]] / mu * [Dx, Dy] * mu
|
||||
|
||||
which is the form used in this file.
|
||||
|
||||
As the z-dependence is known, all the functions in this file assume a 2D grid
|
||||
(ie. dxes = [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dx_h_0, ...], [dy_h_0, ...]]])
|
||||
with propagation along the z axis.
|
||||
"""
|
||||
|
||||
from typing import List, Tuple
|
||||
import numpy
|
||||
from numpy.linalg import norm
|
||||
import scipy.sparse as sparse
|
||||
|
||||
from . import unvec, dx_lists_t, field_t, vfield_t
|
||||
from . import operators
|
||||
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
|
||||
|
||||
def operator(omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
epsilon: vfield_t,
|
||||
mu: vfield_t = None,
|
||||
) -> sparse.spmatrix:
|
||||
"""
|
||||
Waveguide operator of the form
|
||||
|
||||
omega**2 * epsilon * mu +
|
||||
epsilon * [[-Dy], [Dx]] / epsilon * [-Dy, Dx] +
|
||||
[[Dx], [Dy]] / mu * [Dx, Dy] * mu
|
||||
|
||||
for use with a field vector of the form [H_x, H_y].
|
||||
|
||||
This operator can be used to form an eigenvalue problem of the form
|
||||
A @ [H_x, H_y] = wavenumber**2 * [H_x, H_y]
|
||||
|
||||
which can then be solved for the eigenmodes of the system (an exp(-i * wavenumber * z)
|
||||
z-dependence is assumed for the fields).
|
||||
|
||||
:param omega: The angular frequency of the system
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D)
|
||||
:param epsilon: Vectorized dielectric constant grid
|
||||
:param mu: Vectorized magnetic permeability grid (default 1 everywhere)
|
||||
:return: Sparse matrix representation of the operator
|
||||
"""
|
||||
if numpy.any(numpy.equal(mu, None)):
|
||||
mu = numpy.ones_like(epsilon)
|
||||
|
||||
Dfx, Dfy = operators.deriv_forward(dxes[0])
|
||||
Dbx, Dby = operators.deriv_back(dxes[1])
|
||||
|
||||
eps_parts = numpy.split(epsilon, 3)
|
||||
eps_yx = sparse.diags(numpy.hstack((eps_parts[1], eps_parts[0])))
|
||||
eps_z_inv = sparse.diags(1 / eps_parts[2])
|
||||
|
||||
mu_parts = numpy.split(mu, 3)
|
||||
mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1])))
|
||||
mu_z_inv = sparse.diags(1 / mu_parts[2])
|
||||
|
||||
op = omega ** 2 * eps_yx @ mu_xy + \
|
||||
eps_yx @ sparse.vstack((-Dfy, Dfx)) @ eps_z_inv @ sparse.hstack((-Dby, Dbx)) + \
|
||||
sparse.vstack((Dbx, Dby)) @ mu_z_inv @ sparse.hstack((Dfx, Dfy)) @ mu_xy
|
||||
|
||||
return op
|
||||
|
||||
|
||||
def normalized_fields(v: numpy.ndarray,
|
||||
wavenumber: complex,
|
||||
omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
epsilon: vfield_t,
|
||||
mu: vfield_t = None
|
||||
) -> Tuple[vfield_t, vfield_t]:
|
||||
"""
|
||||
Given a vector v containing the vectorized H_x and H_y fields,
|
||||
returns normalized, vectorized E and H fields for the system.
|
||||
|
||||
:param v: Vector containing H_x and H_y fields
|
||||
:param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v
|
||||
:param omega: The angular frequency of the system
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D)
|
||||
:param epsilon: Vectorized dielectric constant grid
|
||||
:param mu: Vectorized magnetic permeability grid (default 1 everywhere)
|
||||
:return: Normalized, vectorized (e, h) containing all vector components.
|
||||
"""
|
||||
e = v2e(v, wavenumber, omega, dxes, epsilon, mu=mu)
|
||||
h = v2h(v, wavenumber, dxes, mu=mu)
|
||||
|
||||
shape = [s.size for s in dxes[0]]
|
||||
dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)]
|
||||
|
||||
E = unvec(e, shape)
|
||||
H = unvec(h, shape)
|
||||
|
||||
S1 = E[0] * numpy.roll(numpy.conj(H[1]), 1, axis=0) * dxes_real[0][1] * dxes_real[1][0]
|
||||
S2 = E[1] * numpy.roll(numpy.conj(H[0]), 1, axis=1) * dxes_real[0][0] * dxes_real[1][1]
|
||||
S = 0.25 * ((S1 + numpy.roll(S1, 1, axis=0)) -
|
||||
(S2 + numpy.roll(S2, 1, axis=1)))
|
||||
P = 0.5 * numpy.real(S.sum())
|
||||
assert P > 0, 'Found a mode propagating in the wrong direction! P={}'.format(P)
|
||||
|
||||
norm_amplitude = 1 / numpy.sqrt(P)
|
||||
norm_angle = -numpy.angle(e[e.size//2])
|
||||
norm_factor = norm_amplitude * numpy.exp(1j * norm_angle)
|
||||
|
||||
e *= norm_factor
|
||||
h *= norm_factor
|
||||
|
||||
return e, h
|
||||
|
||||
|
||||
def v2h(v: numpy.ndarray,
|
||||
wavenumber: complex,
|
||||
dxes: dx_lists_t,
|
||||
mu: vfield_t = None
|
||||
) -> vfield_t:
|
||||
"""
|
||||
Given a vector v containing the vectorized H_x and H_y fields,
|
||||
returns a vectorized H including all three H components.
|
||||
|
||||
:param v: Vector containing H_x and H_y fields
|
||||
:param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D)
|
||||
:param mu: Vectorized magnetic permeability grid (default 1 everywhere)
|
||||
:return: Vectorized H field with all vector components
|
||||
"""
|
||||
Dfx, Dfy = operators.deriv_forward(dxes[0])
|
||||
op = sparse.hstack((Dfx, Dfy))
|
||||
|
||||
if not numpy.any(numpy.equal(mu, None)):
|
||||
mu_parts = numpy.split(mu, 3)
|
||||
mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1])))
|
||||
mu_z_inv = sparse.diags(1 / mu_parts[2])
|
||||
|
||||
op = mu_z_inv @ op @ mu_xy
|
||||
|
||||
w = op @ v / (1j * wavenumber)
|
||||
return numpy.hstack((v, w)).flatten()
|
||||
|
||||
|
||||
def v2e(v: numpy.ndarray,
|
||||
wavenumber: complex,
|
||||
omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
epsilon: vfield_t,
|
||||
mu: vfield_t = None
|
||||
) -> vfield_t:
|
||||
"""
|
||||
Given a vector v containing the vectorized H_x and H_y fields,
|
||||
returns a vectorized E containing all three E components
|
||||
|
||||
:param v: Vector containing H_x and H_y fields
|
||||
:param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v
|
||||
:param omega: The angular frequency of the system
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D)
|
||||
:param epsilon: Vectorized dielectric constant grid
|
||||
:param mu: Vectorized magnetic permeability grid (default 1 everywhere)
|
||||
:return: Vectorized E field with all vector components.
|
||||
"""
|
||||
h2eop = h2e(wavenumber, omega, dxes, epsilon)
|
||||
return h2eop @ v2h(v, wavenumber, dxes, mu)
|
||||
|
||||
|
||||
def e2h(wavenumber: complex,
|
||||
omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
mu: vfield_t = None
|
||||
) -> sparse.spmatrix:
|
||||
"""
|
||||
Returns an operator which, when applied to a vectorized E eigenfield, produces
|
||||
the vectorized H eigenfield.
|
||||
|
||||
:param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v
|
||||
:param omega: The angular frequency of the system
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D)
|
||||
:param mu: Vectorized magnetic permeability grid (default 1 everywhere)
|
||||
:return: Sparse matrix representation of the operator
|
||||
"""
|
||||
op = curl_e(wavenumber, dxes) / (-1j * omega)
|
||||
if not numpy.any(numpy.equal(mu, None)):
|
||||
op = sparse.diags(1 / mu) @ op
|
||||
return op
|
||||
|
||||
|
||||
def h2e(wavenumber: complex,
|
||||
omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
epsilon: vfield_t
|
||||
) -> sparse.spmatrix:
|
||||
"""
|
||||
Returns an operator which, when applied to a vectorized H eigenfield, produces
|
||||
the vectorized E eigenfield.
|
||||
|
||||
:param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v
|
||||
:param omega: The angular frequency of the system
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D)
|
||||
:param epsilon: Vectorized dielectric constant grid
|
||||
:return: Sparse matrix representation of the operator
|
||||
"""
|
||||
op = sparse.diags(1 / (1j * omega * epsilon)) @ curl_h(wavenumber, dxes)
|
||||
return op
|
||||
|
||||
|
||||
def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
|
||||
"""
|
||||
Discretized curl operator for use with the waveguide E field.
|
||||
|
||||
:param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D)
|
||||
:return: Sparse matrix representation of the operator
|
||||
"""
|
||||
n = 1
|
||||
for d in dxes[0]:
|
||||
n *= len(d)
|
||||
|
||||
Bz = -1j * wavenumber * sparse.eye(n)
|
||||
Dfx, Dfy = operators.deriv_forward(dxes[0])
|
||||
return operators.cross([Dfx, Dfy, Bz])
|
||||
|
||||
|
||||
def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
|
||||
"""
|
||||
Discretized curl operator for use with the waveguide H field.
|
||||
|
||||
:param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D)
|
||||
:return: Sparse matrix representation of the operator
|
||||
"""
|
||||
n = 1
|
||||
for d in dxes[1]:
|
||||
n *= len(d)
|
||||
|
||||
Bz = -1j * wavenumber * sparse.eye(n)
|
||||
Dbx, Dby = operators.deriv_back(dxes[1])
|
||||
return operators.cross([Dbx, Dby, Bz])
|
||||
|
||||
|
||||
def h_err(h: vfield_t,
|
||||
wavenumber: complex,
|
||||
omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
epsilon: vfield_t,
|
||||
mu: vfield_t = None
|
||||
) -> float:
|
||||
"""
|
||||
Calculates the relative error in the H field
|
||||
|
||||
:param h: Vectorized H field
|
||||
:param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v
|
||||
:param omega: The angular frequency of the system
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D)
|
||||
:param epsilon: Vectorized dielectric constant grid
|
||||
:param mu: Vectorized magnetic permeability grid (default 1 everywhere)
|
||||
:return: Relative error norm(OP @ h) / norm(h)
|
||||
"""
|
||||
ce = curl_e(wavenumber, dxes)
|
||||
ch = curl_h(wavenumber, dxes)
|
||||
|
||||
eps_inv = sparse.diags(1 / epsilon)
|
||||
|
||||
if numpy.any(numpy.equal(mu, None)):
|
||||
op = ce @ eps_inv @ ch @ h - omega ** 2 * h
|
||||
else:
|
||||
op = ce @ eps_inv @ ch @ h - omega ** 2 * (mu * h)
|
||||
|
||||
return norm(op) / norm(h)
|
||||
|
||||
|
||||
def e_err(e: vfield_t,
|
||||
wavenumber: complex,
|
||||
omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
epsilon: vfield_t,
|
||||
mu: vfield_t = None
|
||||
) -> float:
|
||||
"""
|
||||
Calculates the relative error in the E field
|
||||
|
||||
:param e: Vectorized E field
|
||||
:param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v
|
||||
:param omega: The angular frequency of the system
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D)
|
||||
:param epsilon: Vectorized dielectric constant grid
|
||||
:param mu: Vectorized magnetic permeability grid (default 1 everywhere)
|
||||
:return: Relative error norm(OP @ e) / norm(e)
|
||||
"""
|
||||
ce = curl_e(wavenumber, dxes)
|
||||
ch = curl_h(wavenumber, dxes)
|
||||
|
||||
if numpy.any(numpy.equal(mu, None)):
|
||||
op = ch @ ce @ e - omega ** 2 * (epsilon * e)
|
||||
else:
|
||||
mu_inv = sparse.diags(1 / mu)
|
||||
op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e)
|
||||
|
||||
return norm(op) / norm(e)
|
301
fdfd_tools/waveguide_mode.py
Normal file
301
fdfd_tools/waveguide_mode.py
Normal file
@ -0,0 +1,301 @@
|
||||
from typing import Dict, List
|
||||
import numpy
|
||||
import scipy.sparse as sparse
|
||||
import scipy.sparse.linalg as spalg
|
||||
|
||||
from . import vec, unvec, dx_lists_t, vfield_t, field_t
|
||||
from . import operators, waveguide, functional
|
||||
|
||||
|
||||
def solve_waveguide_mode_2d(mode_number: int,
|
||||
omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
epsilon: vfield_t,
|
||||
mu: vfield_t = None,
|
||||
wavenumber_correction: bool = True
|
||||
) -> Dict[str, complex or field_t]:
|
||||
"""
|
||||
Given a 2d region, attempts to solve for the eigenmode with the specified mode number.
|
||||
|
||||
:param mode_number: Number of the mode, 0-indexed
|
||||
:param omega: Angular frequency of the simulation
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:param epsilon: Dielectric constant
|
||||
:param mu: Magnetic permeability (default 1 everywhere)
|
||||
:param wavenumber_correction: Whether to correct the wavenumber to
|
||||
account for numerical dispersion (default True)
|
||||
:return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex}
|
||||
"""
|
||||
|
||||
'''
|
||||
Solve for the largest-magnitude eigenvalue of the real operator
|
||||
by using power iteration.
|
||||
'''
|
||||
dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes]
|
||||
|
||||
A_r = waveguide.operator(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu))
|
||||
|
||||
# Use power iteration for 20 steps to estimate the dominant eigenvector
|
||||
v = numpy.random.rand(A_r.shape[0])
|
||||
for _ in range(20):
|
||||
v = A_r @ v
|
||||
v /= numpy.linalg.norm(v)
|
||||
|
||||
lm_eigval = v @ A_r @ v
|
||||
|
||||
'''
|
||||
Shift by the absolute value of the largest eigenvalue, then find a few of the
|
||||
largest (shifted) eigenvalues. The shift ensures that we find the largest
|
||||
_positive_ eigenvalues, since any negative eigenvalues will be shifted to the range
|
||||
0 >= neg_eigval + abs(lm_eigval) > abs(lm_eigval)
|
||||
'''
|
||||
shifted_A_r = A_r + abs(lm_eigval) * sparse.eye(A_r.shape[0])
|
||||
eigvals, eigvecs = spalg.eigs(shifted_A_r, which='LM', k=mode_number + 3, ncv=50)
|
||||
|
||||
# Pick the eigenvalue we want from the few we found
|
||||
k = eigvals.argsort()[-(mode_number+1)]
|
||||
v = eigvecs[:, k]
|
||||
|
||||
'''
|
||||
Now solve for the eigenvector of the full operator, using the real operator's
|
||||
eigenvector as an initial guess for Rayleigh quotient iteration.
|
||||
'''
|
||||
A = waveguide.operator(omega, dxes, epsilon, mu)
|
||||
|
||||
eigval = None
|
||||
for _ in range(40):
|
||||
eigval = v @ A @ v
|
||||
if numpy.linalg.norm(A @ v - eigval * v) < 1e-13:
|
||||
break
|
||||
w = spalg.spsolve(A - eigval * sparse.eye(A.shape[0]), v)
|
||||
v = w / numpy.linalg.norm(w)
|
||||
|
||||
# Calculate the wave-vector (force the real part to be positive)
|
||||
wavenumber = numpy.sqrt(eigval)
|
||||
wavenumber *= numpy.sign(numpy.real(wavenumber))
|
||||
|
||||
e, h = waveguide.normalized_fields(v, wavenumber, omega, dxes, epsilon, mu)
|
||||
|
||||
'''
|
||||
Perform correction on wavenumber to account for numerical dispersion.
|
||||
|
||||
See Numerical Dispersion in Taflove's FDTD book.
|
||||
This correction term reduces the error in emitted power, but additional
|
||||
error is introduced into the E_err and H_err terms. This effect becomes
|
||||
more pronounced as beta increases.
|
||||
'''
|
||||
if wavenumber_correction:
|
||||
wavenumber -= 2 * numpy.sin(numpy.real(wavenumber / 2)) - numpy.real(wavenumber)
|
||||
|
||||
shape = [d.size for d in dxes[0]]
|
||||
fields = {
|
||||
'wavenumber': wavenumber,
|
||||
'E': unvec(e, shape),
|
||||
'H': unvec(h, shape),
|
||||
}
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def solve_waveguide_mode(mode_number: int,
|
||||
omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
axis: int,
|
||||
polarity: int,
|
||||
slices: List[slice],
|
||||
epsilon: field_t,
|
||||
mu: field_t = None,
|
||||
wavenumber_correction: bool = True
|
||||
) -> Dict[str, complex or numpy.ndarray]:
|
||||
"""
|
||||
Given a 3D grid, selects a slice from the grid and attempts to
|
||||
solve for an eigenmode propagating through that slice.
|
||||
|
||||
:param mode_number: Number of the mode, 0-indexed
|
||||
:param omega: Angular frequency of the simulation
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:param axis: Propagation axis (0=x, 1=y, 2=z)
|
||||
:param polarity: Propagation direction (+1 for +ve, -1 for -ve)
|
||||
:param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use
|
||||
as the waveguide cross-section. slices[axis] should select only one
|
||||
:param epsilon: Dielectric constant
|
||||
:param mu: Magnetic permeability (default 1 everywhere)
|
||||
:param wavenumber_correction: Whether to correct the wavenumber to
|
||||
account for numerical dispersion (default True)
|
||||
:return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex}
|
||||
"""
|
||||
if mu is None:
|
||||
mu = [numpy.ones_like(epsilon[0])] * 3
|
||||
|
||||
'''
|
||||
Solve the 2D problem in the specified plane
|
||||
'''
|
||||
# Define rotation to set z as propagation direction
|
||||
order = numpy.roll(range(3), 2 - axis)
|
||||
reverse_order = numpy.roll(range(3), axis - 2)
|
||||
|
||||
# Reduce to 2D and solve the 2D problem
|
||||
args_2d = {
|
||||
'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes],
|
||||
'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]),
|
||||
'mu': vec([mu[i][slices].transpose(order) for i in order]),
|
||||
'wavenumber_correction': wavenumber_correction,
|
||||
}
|
||||
fields_2d = solve_waveguide_mode_2d(mode_number, omega=omega, **args_2d)
|
||||
|
||||
'''
|
||||
Apply corrections and expand to 3D
|
||||
'''
|
||||
# Scale based on dx in propagation direction
|
||||
dxab_forward = numpy.array([dx[order[2]][slices[order[2]]] for dx in dxes])
|
||||
|
||||
# Adjust for propagation direction
|
||||
fields_2d['E'][2] *= polarity
|
||||
fields_2d['H'][2] *= polarity
|
||||
|
||||
# Apply phase shift to H-field
|
||||
d_prop = 0.5 * sum(dxab_forward)
|
||||
for a in range(3):
|
||||
fields_2d['H'][a] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * d_prop)
|
||||
|
||||
# Expand E, H to full epsilon space we were given
|
||||
E = [None]*3
|
||||
H = [None]*3
|
||||
for a, o in enumerate(reverse_order):
|
||||
E[a] = numpy.zeros_like(epsilon[0], dtype=complex)
|
||||
H[a] = numpy.zeros_like(epsilon[0], dtype=complex)
|
||||
|
||||
E[a][slices] = fields_2d['E'][o][:, :, None].transpose(reverse_order)
|
||||
H[a][slices] = fields_2d['H'][o][:, :, None].transpose(reverse_order)
|
||||
|
||||
results = {
|
||||
'wavenumber': fields_2d['wavenumber'],
|
||||
'H': H,
|
||||
'E': E,
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def compute_source(E: field_t,
|
||||
H: field_t,
|
||||
wavenumber: complex,
|
||||
omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
axis: int,
|
||||
polarity: int,
|
||||
slices: List[slice],
|
||||
mu: field_t = None,
|
||||
) -> field_t:
|
||||
"""
|
||||
Given an eigenmode obtained by solve_waveguide_mode, returns the current source distribution
|
||||
necessary to position a unidirectional source at the slice location.
|
||||
|
||||
:param E: E-field of the mode
|
||||
:param H: H-field of the mode (advanced by half of a Yee cell from E)
|
||||
:param wavenumber: Wavenumber of the mode
|
||||
:param omega: Angular frequency of the simulation
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:param axis: Propagation axis (0=x, 1=y, 2=z)
|
||||
:param polarity: Propagation direction (+1 for +ve, -1 for -ve)
|
||||
:param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use
|
||||
as the waveguide cross-section. slices[axis] should select only one
|
||||
:param mu: Magnetic permeability (default 1 everywhere)
|
||||
:return: J distribution for the unidirectional source
|
||||
"""
|
||||
if mu is None:
|
||||
mu = [1] * 3
|
||||
|
||||
J = [None]*3
|
||||
M = [None]*3
|
||||
|
||||
src_order = numpy.roll(range(3), axis)
|
||||
exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]])
|
||||
J[src_order[0]] = numpy.zeros_like(E[0])
|
||||
J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity
|
||||
J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity
|
||||
|
||||
M[src_order[0]] = numpy.zeros_like(E[0])
|
||||
M[src_order[1]] = +numpy.roll(E[src_order[2]], -1, axis=axis)
|
||||
M[src_order[2]] = -numpy.roll(E[src_order[1]], -1, axis=axis)
|
||||
|
||||
A1f = functional.curl_h(dxes)
|
||||
|
||||
Jm_iw = A1f([M[k] / mu[k] for k in range(3)])
|
||||
for k in range(3):
|
||||
J[k] += Jm_iw[k] / (-1j * omega)
|
||||
|
||||
return J
|
||||
|
||||
|
||||
def compute_overlap_e(E: field_t,
|
||||
H: field_t,
|
||||
wavenumber: complex,
|
||||
omega: complex,
|
||||
dxes: dx_lists_t,
|
||||
axis: int,
|
||||
polarity: int,
|
||||
slices: List[slice],
|
||||
mu: field_t = None,
|
||||
) -> field_t:
|
||||
"""
|
||||
Given an eigenmode obtained by solve_waveguide_mode, calculates overlap_e for the
|
||||
mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn)
|
||||
[assumes reflection symmetry].
|
||||
|
||||
overlap_e makes use of the e2h operator to collapse the above expression into
|
||||
(vec(E) @ vec(overlap_e)), allowing for simple calculation of the mode overlap.
|
||||
|
||||
:param E: E-field of the mode
|
||||
:param H: H-field of the mode (advanced by half of a Yee cell from E)
|
||||
:param wavenumber: Wavenumber of the mode
|
||||
:param omega: Angular frequency of the simulation
|
||||
:param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header
|
||||
:param axis: Propagation axis (0=x, 1=y, 2=z)
|
||||
:param polarity: Propagation direction (+1 for +ve, -1 for -ve)
|
||||
:param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use
|
||||
as the waveguide cross-section. slices[axis] should select only one
|
||||
:param mu: Magnetic permeability (default 1 everywhere)
|
||||
:return: overlap_e for calculating the mode overlap
|
||||
"""
|
||||
cross_plane = [slice(None)] * 3
|
||||
cross_plane[axis] = slices[axis]
|
||||
|
||||
# Determine phase factors for parallel slices
|
||||
a_shape = numpy.roll([-1, 1, 1], axis)
|
||||
a_E = numpy.real(dxes[0][axis]).cumsum()
|
||||
a_H = numpy.real(dxes[1][axis]).cumsum()
|
||||
iphi = -polarity * 1j * wavenumber
|
||||
phase_E = numpy.exp(iphi * (a_E - a_E[slices[axis]])).reshape(a_shape)
|
||||
phase_H = numpy.exp(iphi * (a_H - a_H[slices[axis]])).reshape(a_shape)
|
||||
|
||||
# Expand our slice to the entire grid using the calculated phase factors
|
||||
Ee = [None]*3
|
||||
He = [None]*3
|
||||
for k in range(3):
|
||||
Ee[k] = phase_E * E[k][tuple(cross_plane)]
|
||||
He[k] = phase_H * H[k][tuple(cross_plane)]
|
||||
|
||||
|
||||
# Write out the operator product for the mode orthogonality integral
|
||||
domain = numpy.zeros_like(E[0], dtype=int)
|
||||
domain[slices] = 1
|
||||
|
||||
npts = E[0].size
|
||||
dn = numpy.zeros(npts * 3, dtype=int)
|
||||
dn[0:npts] = 1
|
||||
dn = numpy.roll(dn, npts * axis)
|
||||
|
||||
e2h = operators.e2h(omega, dxes, mu)
|
||||
ds = sparse.diags(vec([domain]*3))
|
||||
h_cross_ = operators.poynting_h_cross(vec(He), dxes)
|
||||
e_cross_ = operators.poynting_e_cross(vec(Ee), dxes)
|
||||
|
||||
overlap_e = dn @ ds @ (-h_cross_ + e_cross_ @ e2h)
|
||||
|
||||
# Normalize
|
||||
dx_forward = dxes[0][axis][slices[axis]]
|
||||
norm_factor = numpy.abs(overlap_e @ vec(Ee))
|
||||
overlap_e /= norm_factor * dx_forward
|
||||
|
||||
return unvec(overlap_e, E[0].shape)
|
1
float_raster.py
Symbolic link
1
float_raster.py
Symbolic link
@ -0,0 +1 @@
|
||||
../float_raster/float_raster.py
|
18
setup.py
Normal file
18
setup.py
Normal file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(name='fdfd_tools',
|
||||
version='0.1',
|
||||
description='FDFD Electromagnetic simulation tools',
|
||||
author='Jan Petykiewicz',
|
||||
author_email='anewusername@gmail.com',
|
||||
url='https://mpxd.net/gogs/jan/fdfd_tools',
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
'numpy',
|
||||
'scipy',
|
||||
],
|
||||
extras_require={
|
||||
},
|
||||
)
|
Loading…
Reference in New Issue
Block a user