Skip to content

Concepts

Three pieces, one contract

Piece Runs where Licence
SDK (import photonhub as ph) your machine, pure Python Apache-2.0
Solver (phsolver) your machine or a remote GPU proprietary
Cloud service (via ph.cloud) LeapField infrastructure invitation-gated

The object that crosses every boundary is Simulation. It serialises to JSON against a versioned schema, so the same scene is executable by a local binary, a remote GPU worker, or the desktop application, and comes back as the same RunResult structure. Numerical agreement also depends on the selected engine and settings.

That is why the SDK is useful on its own: building, validating, and pricing a scene needs no engine at all.

Building a scene

A Simulation describes domain extents, grid, run controls, structures, sources, monitors and boundaries. It is a frozen model, so:

  • Construction validates. Structural and cross-field checks run at build time, so an out-of-domain monitor or a malformed polygon raises before a solver starts.
  • It is comparable and hashable. Equality compares model values. Wire serialization represents the resolved solver inputs.
  • Edits produce new objects. Helper methods such as with_auto_mesh() and with_mesh_overrides() return a modified copy. The latter accepts positional MeshOverride objects, plus auto-mesh options.

The SDK resolves mesh cell counts and timesteps. It checks supported grid and dispersion-stability rules, including full-tensor restrictions. check_runnable() checks sources, boundary slabs and monitor positions. It refuses a source inside the PML or absorber layers, one error per mode launch, port or beam, naming the face and the depth; a symmetry plane is a mirror, not a layer, and periodic axes have none. The selected engine remains authoritative via phsolver validate. Available device memory and backend-specific limits still need engine validation or cloud preflight.

Engine provenance

A common schema and result structure do not guarantee identical numbers. Match engine revision, mesh, materials, subpixel settings, boundaries, precision, sources, monitors and run controls. CPU/GPU comparisons use the numerical contract's tolerances, rather than general bit-for-bit equality.

# data from the local quickstart run
print(data.provenance.get("solver_version"))
print(data.provenance.get("git_sha"))

The cloud API and solver images are independently versioned. Grid comparisons in cloud preflight check the quoted grid, not the engine's numerical behavior. An older engine can return plausible numbers. The cloud engine can trail the source. Absorbers thicker than 40 layers are one recorded difference until the cloud engine is rebuilt. Check git_sha in result provenance. See Known limitations.

Units

Lengths are microns and carry a _um suffix (size_um, dl_um, center_um). Frequencies are hertz. Times are seconds. The suffix is part of the field name to make the expected unit explicit. Check it before entering a value. A valid number in the wrong unit can still pass validation.

Pricing before running

Use sim and data from the quickstart's first dipole example for the following snippets.

Every Simulation can price itself with no solver and no GPU:

print(sim.cost_estimate().summary())       # sim from the quickstart

The estimator resolves cell count, timestep and step count with the SDK's rules. Match versions before comparing with an engine or service. The unit is the Tcell-step: 10¹² cell updates, num_cells * num_steps / 1e12.

The rate is a model input, not a price

The default planning rate is overridable and is not a production billing commitment. CostEstimate exposes every field (.num_cells, .num_steps, .usd, .device_memory_bytes, …) so you can substitute your own rate. See quote.

Results are labelled arrays

Monitors come back as xarray objects with named dimensions and physical coordinates:

# data = ph.run_local(sim), from the quickstart
flux = data["flux"]                        # dims ('f',), f in Hz
snap = data["snapshot"]                    # dims ('t','component','z','y','x')
ez   = snap.isel(t=-1).sel(component="Ez")
plane = ez.sel(z=1.5, method="nearest")    # microns

Frequency-domain monitors report the continuous-wave response to the sources as declared: flux in watts, fields in V/m and A/m (the engine normalizes per unit amplitude of the first source and the reader multiplies that amplitude back in). Ratios such as R and T against an empty reference run are amplitude-free. The convention used is recorded in .attrs["normalization"]. Cross-validation workflows turn paired runs into R, T, C_sca, or n_eff; see the Examples.

Where the analysis lives

Everything above the raw fields is Python, in the Apache-2.0 SDK:

  • Mode solving: scalar and full-vector solvers with modes consistent with the FDTD fields that will carry them.
  • Mode launch and readout: mode_launch builds a matched source/monitor pair; transmission turns a finished run into a power spectrum.
  • S-matrix, EME, far field, resonances, spectral completion: see the analysis reference.
  • Inverse design: discrete adjoint topology optimisation (ph.inverse_design).

Because it is Python, you can read exactly what any of it does.

Subpixel smoothing

Material interfaces are averaged with Kottke–Farjadpour–Johnson tensor smoothing rather than snapped to the nearest cell. On axis-aligned and waveguide walls the blend is exact and second-order, so wall positions are not limited to the staircase resolution of the grid. On curved 3-D bodies (spheres, cylinders) the current diagonal blend is a documented approximation that can read absolute scattering cross-sections worse than the plain staircase; set subpixel=False when you need absolute cross-sections of curved scatterers. Smoothing is a Simulation field (subpixel, subpixel_method), so it is recorded in the scene and travels with it.