Results and field monitors¶
When fdtdx.run_fdtd returns, everything the monitors recorded is in arrays.detector_states, keyed by monitor name.
The example is a short simulation of a straight waveguide (about a minute on 8 CPU cores).
Run a short simulation¶
The device and settings of Simulation setup and ports, plus two Poynting-flux monitors and a field monitor, run for 300 fs so the whole pulse crosses the guide:
import fdtdx
import jax
import numpy as np
import gdsfactory as gf
import bifrost as bf
from gdsfactory.technology import LayerLevel, LayerStack
gf.gpdk.PDK.activate()
component = gf.components.straight(length=5) # a 5 um long, 500 nm wide waveguide with ports o1 and o2
layer_stack = LayerStack(
layers={"core": LayerLevel(layer=gf.gpdk.LAYER.WG, zmin=0.0, thickness=0.22, material="core", mesh_order=1)}
)
materials = {"core": fdtdx.Material(permittivity=3.48**2), "clad": fdtdx.Material(permittivity=1.444**2)}
stack = bf.StackDevice.from_layer_stack(
component, layer_stack, materials, background="clad", zrange_layers=["core"],
extend_ports=(("o1", 2.0), ("o2", 2.0)), name="stack",
boundary_offset=(0, 0, 1.6, -1.6, 1.59, -1.59), # a 9 x 3.7 x 3.4 um box: whole cells at 50 nm
)
RESOLUTION = 0.05 # um
config = stack.get_simulation_config(time=300e-15, grid=fdtdx.core.grid.UniformGrid(spacing=RESOLUTION * 1e-6))
wave = fdtdx.WaveCharacter(wavelength=1.55e-6)
pulse = fdtdx.GaussianPulseProfile(
center_wave=wave, spectral_width=fdtdx.WaveCharacter(frequency=0.05 * wave.get_frequency())
)
te0 = dict(mode_index=0, filter_pol="te")
volume, constraints = stack.get_simulation_volume()
objects = [volume, stack]
for obj, placing in (
stack.create_mode_plane_source_at(
"o1", (0.5, 0.3), wave_character=wave, temporal_profile=pulse, port_offset=-0.5, name="source", **te0,
),
# mode monitors: how much of TE0 passes each port
stack.create_mode_overlap_detector_at(
"o1", (0.5, 0.3), direction="in", wave_characters=(wave,), scaling_mode="pulse", name="in", **te0,
),
stack.create_mode_overlap_detector_at(
"o2", (0.5, 0.3), wave_characters=(wave,), scaling_mode="pulse", name="out", **te0,
),
# Poynting-flux monitors: the power through a plane at every time step (no wave of their own, so
# the padding's unit is wavelength_um)
stack.create_poynting_flux_detector_at("o1", (0.5, 0.3), wavelength_um=1.55, direction="in", name="flux_in"),
stack.create_poynting_flux_detector_at("o2", (0.5, 0.3), wavelength_um=1.55, name="flux_out"),
):
objects.append(obj)
constraints += placing
# a field monitor: E on the xy plane through the middle of the core, at the wavelength
field = fdtdx.PhasorDetector(
name="field", wave_characters=(wave,), components=("Ex", "Ey", "Ez"),
scaling_mode="pulse", partial_grid_shape=(None, None, 1),
)
objects.append(field)
constraints += [field.same_size(volume, axes=(0, 1)), field.place_at_center(volume)]
boundaries, placing = fdtdx.boundary_objects_from_config(
fdtdx.BoundaryConfig.from_uniform_bound(thickness=round(1.0 / RESOLUTION)), volume
)
objects += boundaries.values()
constraints += placing
key = jax.random.PRNGKey(0)
objects, arrays, params, config, _ = fdtdx.place_objects(objects, config, constraints, key)
arrays, objects, _ = bf.apply_stack(arrays, objects, {**params, **bf.init_stack_params(objects)}, key)
_, arrays = fdtdx.run_fdtd(arrays=arrays, objects=objects, config=config, key=key)
Read the monitors¶
states = arrays.detector_states # one entry per monitor, by name
# mode monitors: the complex amplitude of TE0, one entry per wave character
a_in = objects["in"].compute_overlap(states["in"])
a_out = objects["out"].compute_overlap(states["out"])
transmission = float(np.ravel(abs(a_out / a_in) ** 2)[0])
print(f"transmission: {transmission:.3f}")
# the Poynting-flux monitors: the power through each plane, one row per time step. Summed over the run it is
# the energy that crossed, so the ratio of the two is the fraction of the light that got through
flux_in = np.asarray(states["flux_in"]["poynting_flux"]).sum()
flux_out = np.asarray(states["flux_out"]["poynting_flux"]).sum()
print(f"flux ratio: {float(flux_out / flux_in):.3f}")
# the field monitor: a phasor per component, on the monitor's grid
phasor = np.asarray(states["field"]["phasor"][0, 0, :, :, :, 0]) # (3 components, nx, ny)
intensity = np.sum(np.abs(phasor) ** 2, axis=0) # |E|^2
print("field grid:", intensity.shape, "peak |E|^2:", float(intensity.max()))
Output:
- Mode monitors (
ModeOverlapDetector) give the complex amplitude of a guided mode on their plane:monitor.compute_overlap(states[name]), one entry per wave character. The squared ratio of the output to the input amplitude is the transmission, normalized without a reference run because both monitors measure the same mode of the same guide. A straight guide transmits about 1: a quick check of the source, the monitors and the run time. - Poynting-flux monitors (
PoyntingFluxDetector) record the power through their plane at every time step, in fdtdx's units. Summed over the run it is the energy that crossed, so the ratio of two of them cross-checks the transmission without assuming a mode (it also counts light in other modes). - Field monitors (
PhasorDetector) record the field's complex amplitude at the wave characters you give them, on the monitor's grid:states[name]["phasor"][0, 0, :, :, :, 0]is(components, nx, ny)for an xy plane one cell thick.|E|²is the sum of the squared magnitudes over the components. For the field at many times, aFieldDetectorrecords the same plane at time steps you choose (the bend example records 40 of them, which the viewer plays as a movie).
Choosing a monitor¶
| You want | Use | Read it with |
|---|---|---|
| the power in a guided mode (transmission) | create_mode_overlap_detector_at |
compute_overlap(state) |
| the power through a plane, any mode | create_poynting_flux_detector_at |
state["poynting_flux"] |
| the field's amplitude and phase at a wavelength | PhasorDetector |
state["phasor"] |
| the field at a sequence of times (a movie) | FieldDetector with an OnOffSwitch(interval=...) |
the recorded frames |
| the energy in a region | create_energy_detector_at |
state["energy"] |
Mode and flux monitors are placed on ports (see Simulation setup and ports); a field monitor is any fdtdx detector, sized and placed with constraints, as in the script.
Open the result in the viewer¶
viewer = bf.view(stack)
viewer.load_result(objects, arrays, config, stack=stack) # shows the monitors' fields in the Result tab
viewer.load_result takes what fdtdx hands out: the placed objects, the arrays and the config. The Result tab then
has one read-only entry per monitor, with the field of every plane-shaped monitor drawn on its plane and a frame slider
for monitors that recorded several frames; the Sources / Detectors tab lists the same objects. In a notebook, end the
cell with viewer. More in Viewer.
Troubleshooting¶
- The transmission is far from 1 for a straight guide. The run was too short: the output monitor must see the whole
pulse. Make
timelonger than the pulse needs to cross the device (at 200 fs this script reported 0.87). - A monitor reports nothing, or the viewer shows no field for it. The viewer draws monitors that record a plane (one
cell thick along its normal), and not ones that sum over their volume (
reduce_volume=True). - The numbers are tiny or huge. fdtdx's flux and amplitudes are in its own units; compare ratios, not absolute values.