Setting Up Simulations

From a GDS layout to S-parameters in four steps, on any engine.

1. Load the layout

from gds_fdtd.technology import Technology
from gds_fdtd.lyprocessor import load_cell
from gds_fdtd.simprocessor import load_component_from_tech

tech = Technology.from_yaml("examples/tech.yaml")
cell, layout = load_cell("examples/devices.gds", top_cell="crossing_te1550")
component = load_component_from_tech(cell=cell, tech=tech)

Ports are auto-detected from the SiEPIC pin convention (never hand-placed). gdsfactory (>= 9) components convert directly:

import gdsfactory as gf
from gds_fdtd.layout.gdsfactory import from_gdsfactory

gf.gpdk.PDK.activate()
component = from_gdsfactory(gf.components.straight(length=5), tech)

plot_component(component, spec=spec) shows what you loaded, device polygons, the auto-detected ports, the DevRec bounds, and the FDTD region:

_images/ybranch_geometry.png

2. Configure with SimulationSpec

Every numeric setting lives in one validated model (lengths in µm, angles in degrees, frequencies in Hz, package-wide):

from gds_fdtd.spec import SimulationSpec

spec = SimulationSpec(
    wavelength_start=1.5,
    wavelength_end=1.6,
    wavelength_points=51,
    mesh=10,                      # grid cells per wavelength
    boundary=("PML", "PML", "Metal"),
    symmetry=(0, 0, 0),
    z_min=-1.0,
    z_max=1.11,
    modes=(1, 2),                 # TE + TM
    field_monitors=("z",),
)

Bad values fail loudly at construction with the offending field named.

Field monitors are steerable: one plane per axis in field_monitors ("z" top view, "y"/"x" side views), each sitting at the domain center (x/y) or the device layers’ average mid-plane (z) unless field_monitor_positions pins it to an absolute coordinate. On tidy3d, field_monitor_wavelengths restricts what the monitors record, so a dense S-parameter spectrum does not force an equally dense field download. See where every plane sits before running anything:

spec = SimulationSpec(
    field_monitors=("y", "z"),
    field_monitor_positions={"z": 0.11},      # pin to the Si-core mid-plane
    field_monitor_wavelengths=(1.545, 1.59),  # record just these [um]
)

from gds_fdtd.plotting import plot_monitor_planes
plot_monitor_planes(solver)   # offline: domain, layers, every plane labelled

05b · Field monitors: where they sit, what they record walks through the placement machinery on the Si→SiN escalator, and 11 · A Bragg grating: the stopband, seen in the field uses the wavelength selection to watch one device reflect in-band and transmit out-of-band from a single run.

The whole setup also renders as an interactive 3D scene — the extruded layer stack with per-layer colors, the port cones, each port’s mode plane at its real width_ports × depth_ports (with a dimensions label), the extension stubs, the monitor planes, and the domain box; orbit, zoom, click an object for its material and z-extent, and toggle groups from the legend:

from gds_fdtd.viewer3d import show_3d, save_3d, render_static

show_3d(solver)                     # notebooks AND these docs (three.js)
save_3d(solver, "device_3d.html")   # standalone shareable page
render_static(solver)               # matplotlib, for JS-free contexts

The scene shows true 1:1:1 proportions by default (a 220 nm layer really is hair-thin beside a many-µm device); the legend’s z ×4 checkbox exaggerates the vertical axis when you want to inspect a thin stack. render_static draws the identical scene with matplotlib for the README and other JS-free contexts:

_images/escalator_3d.png

render_static of the Si→SiN escalator solver: the two cores, the translucent cladding, the port mode planes, the monitor planes, and the domain box — the same scene show_3d renders interactively.

The 3D views embedded in 01 · From a GDS layout to a Component, 05b · Field monitors: where they sit, what they record, and 11 · A Bragg grating: the stopband, seen in the field are live — try them.

3. Validate, build, estimate, all free

from gds_fdtd.solvers import get_solver

solver = get_solver("tidy3d")(component, technology=tech, spec=spec)
print(solver.describe())
assert solver.validate() == []
artifacts = solver.build()       # offline: full native scene / setup script
print(solver.estimate())

Nothing so far cost anything: no cloud tasks, no license checkout.

4. Run and analyze

smatrix = solver.run()                        # the only spending step
smatrix.is_reciprocal(), smatrix.is_passive() # physics checks
smatrix.to_dat("device.dat")                  # -> INTERCONNECT
smatrix.to_touchstone("device.s4p")           # -> scikit-rf & friends

from gds_fdtd.plotting import plot_smatrix
plot_smatrix(smatrix, kind="db")
solver.plot_fields(axis="z")
_images/ybranch_sparams.png

plot_smatrix, every measured path in dB. This y-branch splits its input near −3 dB into each arm.

_images/ybranch_field.png

solver.plot_fields(axis="z"), the |E|² field in the device plane, the mode splitting one input into two.

Choosing the mesh

Sweep the mesh and let the S-matrix convergence decide. With a cache directory, repeating a sweep never re-spends:

from gds_fdtd.convergence import sweep

report = sweep(get_solver("tidy3d"), component, tech, spec,
               field="mesh", values=[6, 8, 10], cache_dir=".gds_fdtd_cache")
print(report.summary())
report.recommend(tol_db=0.05)    # first value that moved < 0.05 dB

Ship it anywhere

A simulation is a serializable job; any machine with the package is a worker (see Running gds_fdtd jobs on remote compute):

gds-fdtd validate job.json
gds-fdtd estimate job.json
gds-fdtd run job.json --out results/

Budgets (max_flexcredits / max_wall_seconds) are enforced before and during the run; secrets come from the environment only.