Adding your own solver¶
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()
The contract¶
Subclass gds_fdtd.solvers.Solver and implement:
method |
must |
must NOT |
|---|---|---|
|
store the job (handled by the base class) |
touch disk, network, licenses, GPUs — constructors are cheap and pure |
|
return every problem with the job as a human-readable string; |
raise for foreseeable problems (return them instead) |
|
produce the engine-native scene offline and deterministically (two calls → identical artifacts) |
contact anything: no cloud, no license checks |
|
give offline cost hints (cells, memory, #sims) |
spend anything |
|
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"
)
What the base class gives you¶
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.
Returning results¶
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)
Registration¶
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).
Testing — you inherit a suite¶
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).
Checklist¶
[ ] 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