09 · The CLI and serializable jobs¶
Everything so far has been Python. For remote and batch compute you want a
job you can hand to another machine — a cluster node, a cloud runner — without
shipping your session. gds_fdtd gives you two things for that:
a
JobSpec: one JSON document that fully describes a simulation (layout file, technology file,SimulationSpec, engine, budget) — no live objects, no secrets;the
gds-fdtdCLI:solvers | validate | build | estimate | run | convert | convert-tech, with an explicit exit-code contract.
validate / build / estimate are offline and free — the CLI previews a
job (and its cost) before run ever touches an engine. This whole notebook
runs offline.
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from gds_fdtd.execution import JobSpec
# Run from the repo root so every path in the job is relative and portable.
_root = next(b for b in (Path.cwd(), *Path.cwd().parents) if (b / "pyproject.toml").exists())
os.chdir(_root)
def _cli(*args: str) -> None:
"""Run `python -m gds_fdtd.cli ...` and show the exit code + output."""
r = subprocess.run(
[sys.executable, "-m", "gds_fdtd.cli", *args], capture_output=True, text=True
)
print(f"$ gds-fdtd {' '.join(args)} → exit {r.returncode}")
print((r.stdout or r.stderr).rstrip() or "(no output)")
1 · A job as a file¶
A JobSpec references its layout and technology by path and carries the
validated SimulationSpec. We point it at the Si→SiN escalator layout from
10_cookbook and serialize the whole job to JSON.
workdir = Path(tempfile.mkdtemp(prefix="gdsfdtd_job_"))
job = JobSpec(
gds_path="examples/10_cookbook/si_sin_escalator.gds",
technology_path="examples/tech.yaml",
solver="beamz",
spec={"wavelength_start": 1.5, "wavelength_end": 1.6, "wavelength_points": 5, "mesh": 6,
"z_min": -1.0, "z_max": 1.11},
)
job.to_file(workdir / "job.json")
print((workdir / "job.json").read_text())
{
"gds_path": "examples/10_cookbook/si_sin_escalator.gds",
"top_cell": null,
"technology_path": "examples/tech.yaml",
"spec": {
"wavelength_start": 1.5,
"wavelength_end": 1.6,
"wavelength_points": 5,
"mesh": 6,
"boundary": [
"PML",
"PML",
"PML"
],
"symmetry": [
0,
0,
0
],
"z_min": -1.0,
"z_max": 1.11,
"width_ports": 2.0,
"depth_ports": 1.5,
"buffer": 1.0,
"modes": [
1
],
"mode_freq_pts": 3,
"run_time_factor": 3.0,
"field_monitors": [
"z"
]
},
"solver": "beamz",
"solver_options": {},
"budget": null
}
The job is pure JSON — safe to commit, ship, or queue. Secrets (API keys, license servers) stay in the environment of whatever machine runs it, never in the file. It round-trips exactly:
reloaded = JobSpec.from_file(workdir / "job.json")
print("round-trips identically:", reloaded == job)
round-trips identically: True
2 · Driving it from the shell¶
solvers reports the engines registered on this machine and their cost model —
handy before you pick one for a job:
_cli("solvers")
$ gds-fdtd solvers → exit 0
beamz ok execution=local cost=free multimode=False
lumerical ok execution=local cost=licensed multimode=True
tidy3d ok execution=cloud cost=credits multimode=True
validate / estimate / build preview the job offline (exit 0 = ready;
2 = the job has problems; 3 = the engine isn’t usable here; 4 = over budget):
_cli("validate", str(workdir / "job.json"))
_cli("estimate", str(workdir / "job.json"))
_cli("build", str(workdir / "job.json"))
$ gds-fdtd validate /var/folders/44/xc__wmg576n7crzyny00z3j80000gp/T/gdsfdtd_job_7q2yai78/job.json → exit 0
valid: True
problems: []
$ gds-fdtd estimate /var/folders/44/xc__wmg576n7crzyny00z3j80000gp/T/gdsfdtd_job_7q2yai78/job.json → exit 0
● Done: Raster cache hit (3d): 564a94fc471723166acc7b252b3f32472ee51d17081d05fd1a597a4b1c6cfbed.npz | load=0.04s
grid_cells: 1960980
memory_gb: 0.09412704
n_simulations: 2
cost_hint: free local compute (JAX; CPU works, GPU if available)
$ gds-fdtd build /var/folders/44/xc__wmg576n7crzyny00z3j80000gp/T/gdsfdtd_job_7q2yai78/job.json → exit 0
● Done: Raster cache hit (3d): 564a94fc471723166acc7b252b3f32472ee51d17081d05fd1a597a4b1c6cfbed.npz | load=0.04s
files: {}
summary: {'n_ports': 2, 'grid_shape': (70, 87, 322), 'dx_nm': 74.31914077483697, 'n_core': 3.476, 'n_clad': 1.444, 'n_simulations': 2}
3 · Running — locally or crash-isolated¶
run executes the job; --backend subprocess runs it in a separate process
(so one bad job can’t take down a sweep) and enforces the job’s wall-clock
budget. The subprocess backend re-invokes this same CLI, which is why
the JSON boundary matters — the child rebuilds the job from the file alone:
gds-fdtd run job.json --out results/ # in-process
gds-fdtd run job.json --out results/ --backend subprocess # crash-isolated
from gds_fdtd.execution import SubprocessBackend
handle = SubprocessBackend().submit(job, out_dir="results")
result = handle.result() # JobResult: smatrix_path, job_hash, wall_seconds, ...
run is the only verb that spends — and a Budget on the JobSpec
(max_flexcredits, max_wall_seconds) lets the CLI refuse before spending
(exit 4). See docs/remote_compute.md for Modal / AWS / SLURM recipes.
Recap & next¶
A simulation is a portable JSON JobSpec; the gds-fdtd CLI previews it for
free and runs it locally or on a cluster, with budgets enforced up front. Next:
10_cookbook — reference devices with known-good S-parameters.