Solvers

Every engine sets up the same way, swap engines by changing one string:

from gds_fdtd.technology import Technology
from gds_fdtd.lyprocessor import load_cell
from gds_fdtd.simprocessor import load_component_from_tech
from gds_fdtd.solvers import get_solver
from gds_fdtd.spec import SimulationSpec

tech = Technology.from_yaml("tech.yaml")       # ONE tech, every engine
cell, layout = load_cell("devices.gds", top_cell="crossing_te1550")
component = load_component_from_tech(cell=cell, tech=tech)

solver = get_solver("tidy3d")(                 # or "lumerical" / "beamz"
    component,
    technology=tech,
    spec=SimulationSpec(wavelength_points=51, mesh=10, z_min=-1.0, z_max=1.11),
)

The lifecycle contract

method

contract

validate() -> list[str]

every problem with the job as human-readable strings; [] = runnable

build() -> SetupArtifacts

engine-native scene, offline and deterministic: no network, no license

estimate() -> ResourceEstimate

offline cost hints (cells, memory, number of simulations)

run() -> SMatrix

the only method that spends money, license seats, or GPU time

assert solver.validate() == []
artifacts = solver.build()      # free: script/scene generated locally
print(solver.estimate())
smatrix = solver.run()          # cloud credits / license / local compute

Available engines

engine

execution

cost

install

Tidy3D >= 2.11

cloud

FlexCredits

pip install gds_fdtd[tidy3d] + TIDY3D_API_KEY

Ansys Lumerical FDTD 2024/2025

local

license

Lumerical install with lumapi on path

beamz >= 0.4.3, < 0.5

local (JAX CPU/GPU)

free

pip install gds_fdtd[beamz]

gds-fdtd solvers (CLI) lists every registered engine with availability and, when unavailable, the reason. Per-engine verification against the real engines lives in SOLVER_STATUS.md, the three engines agree within 0.052 dB (tidy3d ↔ Lumerical within 0.0033 dB) on an identical job.

Standard visualization flow

Every example follows the same four steps:

from gds_fdtd.plotting import plot_component, plot_smatrix

plot_component(component, spec=solver.spec)   # 1: geometry + ports + FDTD region
solver.build()                                # 2: offline setup (free)
plot_smatrix(smatrix, kind="db")              # 3: S-parameters
solver.plot_fields(axis="z")                  # 4: field profile (after run())

Beyond one engine

from gds_fdtd.convergence import sweep
from gds_fdtd.validation import validate_across

# principled mesh choice; with cache_dir reruns are free
report = sweep(get_solver("tidy3d"), component, tech, spec,
               field="mesh", values=[6, 8, 10], cache_dir=".cache")
report.recommend(tol_db=0.05)

# cross-validate: identical job, several engines, worst |dS| in dB
report = validate_across(
    [get_solver("tidy3d"), get_solver("lumerical"), get_solver("beamz")],
    component, tech, spec, cache_dir=".cache",
)
_images/three_engine_agreement.png

The same y-branch on all three engines. Cross-validation quantifies the worst-case disagreement (here a few hundredths of a dB on the split); 07 · Choosing an engine — three solvers, one job walks through it.

Bring your own engine

Any FDTD engine becomes a gds_fdtd solver by implementing the four methods above, see Adding your own solver for the full guide, including the conformance test suite your adapter inherits for free.

Note

The pre-0.5 class interface (fdtd_solver_tidy3d / fdtd_solver_lumerical with per-solver keyword arguments) was removed in 0.6. Use get_solver(name)(component, tech, spec).