Skip to content

Latest commit

 

History

History
120 lines (96 loc) · 5.21 KB

File metadata and controls

120 lines (96 loc) · 5.21 KB

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
__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"
    )

What the base class gives you

  • self.component — geometry: flat, role-tagged Structures (device / substrate / superstrate), Ports with positions, widths, directions, and bounds. Add port-extension stubs with port.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 (constant nk, refractiveindex.info rii, 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 solvers lists 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 | None returning the human-readable reason it can't run here (or None when 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:

  1. offline scene assertions (your build() output for a known fixture),
  2. one real physics smoke on a tiny straight waveguide (|S21|² > 0.8, |S11|² < 0.1) — marked physics/licensed/cloud per the marker taxonomy so CI never spends your money,
  3. if you record real results, sanitize them (tests/recorded/README.md).

Checklist

  • constructor pure; validate/build/estimate offline; only run() spends
  • capabilities accurate; 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_entries with 1-based mode ids; reciprocity/passivity pass
  • entry point registered; conformance suite green
  • secrets from environment variables only — never in code or job files