Skip to content

GDSFactory FDTD guide

gsim.fdtd separates simulation setup into geometry, materials, source, monitors, domain, and solver concerns. It generates the transfer mesh and validated config.json, submits one source to one cloud job, and returns typed results.

Use Simulation to configure a run and FDTDResult to inspect its output.

Configure and run

from gsim import fdtd

sim = fdtd.Simulation(pdk=gpdk)
sim.materials(background="SiO2")
sim.geometry(component=mmi, geometry_tolerance_nm=10)
sim.source(
    port="o1",
    wavelength_um=1.55,
    wavelength_span_um=0.1,
    num_wavelengths=101,
)
sim.domain(padding=0.75, pml_cells=16)
sim.solver(
    cell_size_nm=40,
    energy_decay_fraction=1e-6,
    max_wall_seconds=3600,
)

result = sim.run(check_cache=True)
result.plot()
result.plot_plotly()

The transfer mesh preserves the resolved CAD boundaries exactly and uses coarse tetrahedra in volume interiors. Its default maximum target size is 1 µm; mesh_size_nm remains available as an advanced override. Gmsh conforms to every explicit component, hole, gap, tooth, and port edge even when it is smaller than that target. geometry_tolerance_nm is a geometry-error bound, not a mesh edge size.

Transfer meshing is independent of cell_size_nm, which requests the actual Yee grid used by the FDTD solver and defaults to 60 nm. Source sweeps default to 101 wavelengths. Guided-port monitors are implicit; one port source returns one S-matrix column.

Linear PDK sidewall angles are represented as continuous ruled solids between their exact bottom and top contours. If polygon offsetting changes component, hole, or edge topology, meshing safely falls back to vertical slices bounded by geometry_tolerance_nm instead of constructing an invalid loft.

Use write(path) to generate mesh.msh and config.json without submitting. The original flat constructor keywords remain accepted for compatibility.

Material dispersion

By default, each material reaches the solver as its index at the source wavelength. A broadband run therefore uses that one index at every wavelength. write() and run() emit one warning that names every material whose card varies over the source band, and by how much.

sim.materials(dispersion=True) sends those cards as dispersive models instead. The deployed GDSFactory FDTD solver does not accept them yet, so a run with dispersion=True currently fails on the cloud.

Material card config.json material entry with dispersion=True
Constant, lossless refractive_index
Sellmeier, lossless dispersion.sellmeier, with wavelength_range_nm
Anything else (tables, equations, loss) dispersion.table of n and k

Tables are sampled at 21 evenly spaced wavelengths, plus the knots of a tabulated card inside the band, so that narrow features are kept. The sampled range is the source band plus a 10 % margin on each side; a side keeps its margin only if the card is still valid there. A card that does not cover the band itself raises FDTDConfigError.

A continuous-wave or zero-span source needs only the index at its wavelength, so every material is written as a constant there. The solver encodes loss only in a dispersion table, so a lossy card raises unless the source is pulsed and dispersion=True. sim.materials(overrides={"si": 3.47}) always gives a constant material.

Domain padding is specified in micrometers, using padding as a shared fallback and padding_x, padding_y, or padding_z to override individual axes. Each accepts a nonnegative scalar (equal padding on both sides) or a (negative_side, positive_side) pair. Zero padding is allowed.

sim.domain(
    padding=1,
    padding_x=(0, 2),  # -x, +x
    padding_y=0.5,    # both y sides
    padding_z=(1, 2), # below, above
)

Explicit axis padding is measured from the resolved geometry bounds. Without padding_z, automatic Z sizing uses the PDK background stack when present, otherwise the shared padding. The shared fallback retains automatic guided-port alignment. Explicit axis padding must be zero on a side containing a guided port: waveguide extensions into padding are not currently supported, and a nonzero value on that side raises an error. For a bend with an input on the negative X face and an output on the positive Y face, use padding_x=(0, 1) and padding_y=(1, 0) to pad its other sides.

The old padding_um domain argument and background_padding_um constructor argument have been removed; configure sim.domain(padding=...) instead.

Optional x_bounds, y_bounds, and z_bounds tuples set the non-PML physical domain in micrometers. Omitted axes remain automatically sized from the component, PDK stack, and padding. Explicit bounds must contain the geometry, sources, and monitors; guided ports must remain on a domain face. The backend adds PML outside these bounds. The resulting box is written to config.json as grid.non_pml_bounds, in nanometers. Do not combine explicit bounds and explicit per-axis padding on the same axis; the shared padding fallback can still be used for other axes.

sim.domain(
    x_bounds=(0.0, 35.0),
    y_bounds=(-12.0, 12.0),
    z_bounds=(-1.0, 1.55),
)

Setup visualization

FDTD setup views use the ported GDSFactory FDTD Three.js viewer and do not depend on PyVista. All physical groups are shown by default. Smaller material groups are listed first, followed by the largest volumetric group and then the ports.

sim.plot_3d()                                   # solid interactive geometry
sim.plot_3d(show_mesh=True)                     # add Gmsh surface edges
sim.plot_3d(zoom_to_cursor=False)               # zoom toward the view center
sim.plot_2d(axis="z", position_um=0.11)         # filled cross-section
sim.plot_2d(axis="x", position_um=0, show_mesh=True)  # add cell edges

Omit position_um to start at the geometry midpoint. The 2D viewer includes a slider that moves the plane through the mesh. Both methods return a standalone MeshViewer. Set show_mesh=True when element edges are useful, or use viewer.save("mesh.html") to share the visualization outside a notebook. Zooming moves toward the pointer by default; set zoom_to_cursor=False to zoom toward the view center instead. The same choice is available in the viewer's Zoom toward controls.

The viewer also summarizes the physical domain, estimated Yee-grid dimensions and total cell count (including PML). These values are estimates; the solver backend is authoritative when it constructs the final grid.

Sources

PortSource requires an explicit sim.source(port="...") selection before artifacts can be written or a job can run. A guided PDK port maps to an eigenmode source. Selecting a vertical_te or vertical_tm PDK port instead derives a Gaussian beam and fiber monitor from the port metadata.

Replace sim.source to use another physical source:

sim.source = fdtd.DipoleSource(
    position_um=(0, 0, 0.11),
    current_axis="z",
    wavelength_um=1.55,
)

sim.source = fdtd.LineCurrentSource(
    position_um=(0, -0.5, 0.11),
    line_axis="y",
    current_axis="z",
    length_um=1,
)

GaussianBeamSource exposes the aperture center and size, propagation and polarization vectors, focal point, waist, and optional background index. Only PortSource on a guided port produces normalized S-parameters. Other source types return outgoing port amplitudes and powers.

Plane monitors

For notebook diagnostics, add center-wavelength field intensity images with one call after configuring geometry, source and solver:

sim.add_monitors()               # Optical port images and an XY overview.
sim.add_monitors(xy=False)       # Alternative: optical port images only.
sim.add_monitors(z_um=0.11)      # Alternative: set the XY height in micrometers.
sim.add_monitors(margin_um=1.0)  # Alternative: more cladding around each port.

These are alternative calls, not a sequence: monitor names must be unique. xy=True is the default; z_um=None chooses the resolved geometry's vertical midpoint and is ignored when xy=False. Each optical port's plane follows its own width and its owning PDK layer's vertical bounds, including negative layer thickness. margin_um=0.75 adds that much cladding on each side of the cross section; set it to zero for only the active region or increase it for wider field tails. This is a geometric window, not an automatically mode-converged aperture. The plane is three grid cells inward along its normal; the XY overview spans the component's bounding box. All these planes use the source center wavelength, record intensity without flux, and leave implicit guided-port responses unchanged. They show the driven field, not independent source eigenmode profiles. Existing custom monitors are preserved, and name collisions fail before adding any of these planes. Changing geometry/source/solver later requires recreating the diagnostic planes. This helper only configures monitors; it does not mesh or submit a job. Explicit domain bounds still need to contain the requested planes. Dimensional suffixes follow the existing center_um/position_um conventions; the domain's padding rename is specific to its directional-padding API.

Additional plane monitors can independently record flux, scalar heatmaps, and Gaussian fiber overlap:

sim.monitors.add_plane(
    name="top",
    center_um=(0, 0, 1),
    size_um=(10, 6, 0),
    normal="+z",
    flux=True,
    heatmap=fdtd.Heatmap(
        quantity="intensity",
        wavelengths_um=[1.55],
    ),
)

For a vertical PortSource, request a field image from its implicit port plane:

sim.source(
    port="o2",
    vertical_monitor_heatmap=fdtd.Heatmap(
        quantity="abs_e",
        wavelengths_um=[1.55],
    ),
)

Monitor names are unique. Use add(), remove(), and clear() to manage the collection. A plane's size must be zero along its normal.

Results

result.s_parameters.plot()
result.s_parameters.plot_plotly()
s_parameter_table = result.s_parameters.to_dataframe()

result.port_outputs.plot(quantity="modal_power")
port_table = result.port_outputs.to_dataframe()

# Net monitor flux can be useful as a diagnostic normalization.
result.plot_plotly(normalize_to="top")

# For a Gaussian source, use its analytic incident power and mask weak tails.
coupling = fdtd.gaussian_coupling_efficiency(
    result, sim.source, port="o1"
)
coupling.plot_plotly()

# An eigenmode-normalized fiber overlap is an amplitude; plot its power.
fiber = fdtd.fiber_coupling_efficiency(result.monitors["fiber"])
fiber.plot_plotly()

top = result.monitors["top"]
top.plot_flux()
top.plot_heatmap(wavelength_um=1.55)

Heatmap arrays are loaded lazily from their .npy sidecars. S-parameter plots gap samples flagged below the source noise floor by default. Convergence, grid, timing, and resolved runtime settings remain available on the result object.

Simulation setup views remain separate from these result plots: sim.plot_*() inspects the submitted geometry and mesh, while result.plot*() displays solver output.

API reference