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.
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.