Euler-like waveguide bend: broadband bend loss#

A tangent-continuous 90-degree silicon route connects an x-directed mode source to a y-directed output monitor.

This executable notebook follows the same progression as the official Tidy3D example: inspect parameters, construct geometry and ports, plot the actual voxelized scene, execute one broadband FDTDX simulation, visualize the recorded electric field, and compare the reduced observables with a frozen Tidy3D result.

Imports and accelerator#

import os
os.environ.setdefault("XLA_PYTHON_CLIENT_PREALLOCATE", "false")

from time import perf_counter
import warnings
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import jax
import fdtdx
from IPython import get_ipython

from benchmarks.device_specs import MODE_PORT_SPECS

get_ipython().run_line_magic("matplotlib", "inline")
warnings.filterwarnings("ignore", category=FutureWarning, module=r"tidy3d\.components\.mode")
plt.rcParams.update({"figure.figsize": (9, 4.5), "figure.dpi": 120})
CASE_ID = "device_euler_bend"
UM = 1e-6
spec = MODE_PORT_SPECS[CASE_ID]
print(f"JAX devices: {jax.devices()}")
JAX devices: [CudaDevice(id=0)]

1. Physical and numerical parameters#

parameters = pd.Series(
    {
        "domain (µm)": spec.domain_um,
        "center wavelength (µm)": spec.wavelength0_um,
        "monitor wavelengths (µm)": spec.wavelengths_um,
        "grid pitch (µm)": spec.dl_um,
        "maximum run time (ps)": spec.run_time_s * 1e12,
        "background index": spec.background_index,
        "device thickness (µm)": spec.thickness_um,
        "PML cells per face": spec.pml_layers,
    },
    name="value",
)
display(parameters.to_frame())
print(spec.adaptation)
value
domain (µm) (7.0, 7.0, 1.4)
center wavelength (µm) 1.55
monitor wavelengths (µm) (1.5, 1.55, 1.6)
grid pitch (µm) 0.025
maximum run time (ps) 1.5
background index 1.444
device thickness (µm) 0.21
PML cells per face 8
Smooth 90-degree, tangent-continuous bend retaining the notebook's silicon/oxide cross-section.

2. Geometry and ports#

The plotted vertices below are the arrays passed to fdtdx.ExtrudedPolygon. Port arrows use the same centers and propagation directions passed to the source and mode-overlap detectors.

fig, ax = plt.subplots(figsize=(11, 5.5))
for layer in spec.layers:
    vertices = np.asarray(layer.vertices_um)
    ax.fill(vertices[:, 0], vertices[:, 1], alpha=0.72, label=layer.name)

ports = (spec.input_port, *spec.output_ports)
for port in ports:
    x, y, _ = port.center_um
    delta = 0.55 if port.direction == "+" else -0.55
    dx, dy = (delta, 0) if port.axis == 0 else (0, delta)
    ax.annotate(port.name, (x + dx, y + dy), (x, y), arrowprops={"arrowstyle": "->"})
    ax.scatter([x], [y], s=22, color="black")

ax.set(
    xlabel="x (µm)", ylabel="y (µm)", title="Exact polygon and port geometry",
    xlim=(-spec.domain_um[0] / 2, spec.domain_um[0] / 2),
    ylim=(-spec.domain_um[1] / 2, spec.domain_um[1] / 2),
    aspect="equal",
)
ax.legend(loc="best", fontsize=7)
plt.show()
../../_images/dba70635c7adf71fabaad9bbe75ff79040ba99b2396cca733e28359d07d3b900.png

3. Build the FDTDX scene#

Geometry is extruded through the device layer. PortSpec selects either a solved waveguide mode or an analytic Gaussian source. The optional FieldMonitorSpec records a complex electric-field phasor across the device plane during the same pulsed run used for S-parameters.

domain_m = tuple(value * UM for value in spec.domain_um)
resolution = spec.dl_um * UM
materials = {}
polygons = []
for layer in spec.layers:
    vertices = np.asarray(layer.vertices_um) * UM
    center = 0.5 * (vertices.min(axis=0) + vertices.max(axis=0))
    material = materials.setdefault(
        layer.refractive_index,
        fdtdx.Material(permittivity=layer.refractive_index**2),
    )
    polygon = fdtdx.ExtrudedPolygon(
        name=layer.name,
        vertices=vertices - center,
        axis=2,
        partial_real_shape=(None, None, spec.thickness_um * UM),
        materials={"device": material},
        material_name="device",
        placement_order=layer.placement_order,
        subpixel_smoothing=True,
    )
    polygons.append((polygon, (center[0] + domain_m[0] / 2, center[1] + domain_m[1] / 2, domain_m[2] / 2)))

def make_port(port):
    center = tuple((coordinate + extent / 2) * UM for coordinate, extent in zip(port.center_um, spec.domain_um, strict=True))
    return fdtdx.PortSpec(
        center=center, axis=port.axis, direction=port.direction,
        width=port.width_um * UM, height=port.height_um * UM,
        mode_index=0, filter_pol=port.filter_pol,
        source_kind=port.source_kind,
        gaussian_mode_radius=(port.gaussian_mode_radius_um * UM if port.gaussian_mode_radius_um else None),
        name=port.name,
    )

field_monitor = fdtdx.FieldMonitorSpec(
    name="field_xy",
    center=(domain_m[0] / 2, domain_m[1] / 2, domain_m[2] / 2),
    size=(domain_m[0], domain_m[1], resolution),
    wavelengths=(spec.wavelength0_um * UM,),
    components=("Ex", "Ey", "Ez"),
)
objects, arrays, config = fdtdx.setup_sparams_simulation(
    polygons=polygons,
    input_ports=[make_port(spec.input_port)],
    output_ports=[make_port(port) for port in spec.output_ports],
    wavelength=spec.wavelength0_um * UM,
    wavelengths=tuple(value * UM for value in spec.wavelengths_um),
    resolution=resolution,
    max_time=spec.run_time_s,
    domain_size=domain_m,
    background_material=fdtdx.Material(permittivity=spec.background_index**2),
    pml_layers=spec.pml_layers,
    field_monitors=(field_monitor,),
)
print(f"Yee grid: {objects.volume.grid_shape}")
print(f"Time-step ceiling: {config.time_steps_total:,}")
Yee grid: (296, 296, 72)
Time-step ceiling: 31,470

4. Plot the voxelized simulation geometry#

fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))
fdtdx.plot_material_from_side(config, arrays, "z", position=0.0, ax=axes[0], plot_legend=True)
fdtdx.plot_material_from_side(config, arrays, "y", position=0.0, ax=axes[1], plot_legend=True)
axes[0].set_title("Device plane permittivity")
axes[1].set_title("Vertical permittivity section")
plt.tight_layout()
plt.show()
../../_images/3a84754542965092254727a2e45355b31dace5b90f86adc5ac15f9af967cc858.png

5. Run one broadband simulation#

The mode source, normalization monitor, all output monitors, energy-decay monitor, and spatial field monitor are evaluated together. The simulation may terminate before the time ceiling once residual energy has decayed sufficiently.

started = perf_counter()
sparams, detector_states = fdtdx.calculate_sparam(
    objects, arrays, config, spec.input_port.name, show_progress=False,
)
elapsed_s = perf_counter() - started
print(f"Local FDTDX runtime: {elapsed_s:.1f} s")
Local FDTDX runtime: 42.9 s

6. Plot the simulated electric field#

phasor = np.asarray(detector_states["field_xy"]["phasor"])[0, 0, :3]
field_magnitude = np.sqrt(np.sum(np.abs(phasor) ** 2, axis=0)).squeeze()
field_magnitude /= field_magnitude.max() + 1e-30
fig, ax = plt.subplots(figsize=(11, 5))
image = ax.imshow(
    field_magnitude.T, origin="lower",
    extent=(-spec.domain_um[0] / 2, spec.domain_um[0] / 2, -spec.domain_um[1] / 2, spec.domain_um[1] / 2),
    cmap="magma", vmin=0, vmax=1, aspect="equal",
)
for layer in spec.layers:
    vertices = np.asarray(layer.vertices_um)
    ax.plot(*np.vstack((vertices, vertices[0])).T, color="cyan", lw=0.45, alpha=0.8)
ax.set(xlabel="x (µm)", ylabel="y (µm)", title=f"Executed |E| at {spec.wavelength0_um:.3f} µm")
fig.colorbar(image, ax=ax, label="normalized |E|")
plt.show()
../../_images/33b7a4e439189f5f922860016af5c8ab43740f5cae05177edcdd6832935a7ec3.png

7. Port analysis#

The useful check is broadband fundamental-mode transmission through the 90-degree route.

wavelengths_um = np.asarray(spec.wavelengths_um)
powers = {
    port.name: np.abs(np.asarray(sparams[(port.name, spec.input_port.name)]).squeeze()) ** 2
    for port in spec.output_ports
}
total = sum(powers.values(), start=np.zeros_like(wavelengths_um))
if len(powers) > 1:
    stack = np.stack(list(powers.values()))
    uniformity = np.std(stack, axis=0) / (np.mean(stack, axis=0) + 1e-30)

golden = np.load(f"benchmarks/goldens/{CASE_ID}.npz", allow_pickle=True)
colors = plt.cm.viridis(np.linspace(0.1, 0.9, len(powers)))
fig, ax = plt.subplots(figsize=(9, 4.8))
for (port_name, values), color in zip(powers.items(), colors, strict=True):
    ax.plot(wavelengths_um, values, "o-", color=color, label=f"FDTDX · {port_name}")
    ax.plot(wavelengths_um, golden[port_name], "x--", color=color, alpha=0.65, label=f"Tidy3D · {port_name}")
ax.set(xlabel="wavelength (µm)", ylabel="normalized modal power", title="Port spectra")
ax.grid(alpha=0.25)
ax.legend(ncol=2, fontsize=8)
plt.show()

table = pd.DataFrame({"wavelength_um": wavelengths_um, **powers, "monitored_total": total})
if len(powers) > 1:
    table["relative_port_std"] = uniformity
table
../../_images/ae517491452532c0b4aebbd4db57cd9e03eb151fef9e9471e67e1752de3844a5.png
wavelength_um out monitored_total
0 1.50 10.378269 10.378269
1 1.55 29.453333 29.453333
2 1.60 8.505005 8.505005

Reproduce and refine#

The notebook output above is retained from a real local execution. For a scientific result, rerun after reducing spec.dl_um, increasing the time ceiling, and checking port placement. The benchmark command below applies the catalog’s frozen-reference tolerances and records the result in progress.json.

print("uv run fdtdx-bench run --case device_euler_bend")
uv run fdtdx-bench run --case device_euler_bend