This is the point of gds_fdtd: any FDTD engine becomes a photonics S-parameter solver by implementing four methods. Users then drive your engine with the exact same three lines they use for tidy3d, Lumerical, or beamz:
solver = get_solver("yourengine")(component, tech, spec)
smatrix = solver.run()Subclass gds_fdtd.solvers.Solver and implement:
| method | must | must NOT |
|---|---|---|
__init__ |
store the job (handled by the base class) | touch disk, network, licenses, GPUs — constructors are cheap and pure |
validate() -> list[str] |
return every problem with the job as a human-readable string; [] means runnable |
raise for foreseeable problems (return them instead) |
build() -> SetupArtifacts |
produce the engine-native scene offline and deterministically (two calls → identical artifacts) | contact anything: no cloud, no license checks |
estimate() -> ResourceEstimate |
give offline cost hints (cells, memory, #sims) | spend anything |
run() -> SMatrix |
execute and return the canonical S-matrix | — this is the only method allowed to spend money, license seats, or GPU time |
Declare what your engine can do — these are promises, not probes:
from gds_fdtd.solvers import (
ResourceEstimate, SetupArtifacts, Solver, SolverCapabilities, register_solver,
)
@register_solver
class YourSolver(Solver):
name = "yourengine"
capabilities = SolverCapabilities(
tier="full", # "full" = has its own sources/monitors;
# "kernel" = raw eps in, fields out
execution="local", # or "cloud"
supports_dispersion=False,
supports_sidewall_angle=True,
supports_multimode=False,
supports_gpu=False,
cost_model="free", # or "licensed" / "credits"
)self.component— geometry: flat, role-taggedStructures (device / substrate / superstrate),Ports with positions, widths, directions, andbounds. Add port-extension stubs withport.polygon_extension(buffer=2 * self.spec.buffer)so waveguides terminate through your absorbing boundary, never on a facet.self.technology/Structure.material— materials with neutral hints:gds_fdtd.grid.resolve_index(material, wavelength_um)gives you a complex refractive index offline (constantnk, refractiveindex.inforii, or an already-built tidy3d medium).self.spec— every numeric setting, validated (SimulationSpec).- Helpers:
frequencies_hz(),injection_plan()(per-port source descriptors),domain()(center/span incl. buffer),describe(),run_cached(cache_dir)(free reruns keyed on the full job hash). - Kernel-tier engines (your engine only takes a permittivity grid):
the full pipeline exists —
grid.rasterize(component, ...)→modes.Tidy3DModeSolver().solve(...)(free, local) →extraction.mode_amplitude(fields, mode, ...)turns your recorded DFT fields into S-matrix entries.
Build the canonical SMatrix from per-path entries — unmeasured paths stay
NaN and every exporter (.dat / Touchstone / HDF5), checker (reciprocity,
passivity), and plot works immediately:
from gds_fdtd.smatrix import SMatrix
entries = [] # (in_port, out_port, mode_in, mode_out, f_hz, complex_s)
for excitation in self.injection_plan():
...run, decompose...
entries.append((excitation["name"], out_port, 1, 1, f, s_complex))
return SMatrix.from_entries(entries, name=self.component.name)-
Inside a plugin package — declare an entry point; gds_fdtd discovers it automatically, and
gds-fdtd solverslists it with availability:[project.entry-points."gds_fdtd.solvers"] yourengine = "your_pkg.solver:YourSolver"
-
Availability probing — if your engine may be missing (import, license, GPU), add a
@staticmethod probe_available() -> str | Nonereturning the human-readable reason it can't run here (orNonewhen fine).
The conformance suite (tests/conformance/) parametrizes over every
registered solver: constructor purity (no files, no sockets), deterministic
builds, artifact serialization, estimate-without-network. Register your
class and you inherit ~30 contract tests for free. Add on top:
- offline scene assertions (your
build()output for a known fixture), - one real physics smoke on a tiny straight waveguide
(|S21|² > 0.8, |S11|² < 0.1) — marked
physics/licensed/cloudper the marker taxonomy so CI never spends your money, - if you record real results, sanitize them (
tests/recorded/README.md).
- constructor pure;
validate/build/estimateoffline; onlyrun()spends -
capabilitiesaccurate;probe_available()if the engine can be absent - port extensions through your absorbing boundary (S11 below −25 dB on a straight waveguide is the sign you got this right; −7 dB means you didn't)
-
SMatrix.from_entrieswith 1-based mode ids; reciprocity/passivity pass - entry point registered; conformance suite green
- secrets from environment variables only — never in code or job files