Running simulations¶
Local execution against the phsolver binary, plus the batch and async wrappers. Remote GPU execution lives in ph.cloud.
Run¶
run_local
¶
run_local(
sim: Simulation,
output_dir: Union[str, Path, None] = None,
solver_path: Union[str, Path, None] = None,
progress: Optional[Callable[[dict], None]] = None,
timeout: Optional[float] = None,
device: Union[str, None] = None,
quiet: bool = False,
log_file: Union[str, Path, None] = None,
cancel_event: Optional[Event] = None,
allow_sources_in_boundary: bool = False,
) -> RunResult
Run phsolver run sim.json --output <dir> and load the results.
progress (if given) receives every parsed JSON-lines event dict as it
arrives, and takes over the human surface. When progress is None a
default live status line (field decay vs. the shutoff threshold, phase,
stability, throughput, ETA) is rendered to stderr; pass quiet=True to
silence it. Either way the child runs with --progress none so Python is
the only thing drawing the status line.
log_file (if given) is forwarded to phsolver --log-file so the engine
mirrors the full JSON-lines event stream (start, setup_progress during a
long host setup, progress, done or error: field decay, phase, stability,
throughput) to that path as it runs. The engine
writes it directly, so the record survives even if this process is killed or
crashes; it is independent of progress/quiet. The parent directory is
created if needed.
timeout (seconds) kills the solver and raises. Outputs go to
output_dir (created if needed) or a fresh persistent temp directory.
device selects the backend, "cpu" (default when None), "gpu",
"gpu:N", "gpu:all", or "gpu:N,M,..." (multi-GPU z-decomposition);
it is passed to phsolver
--device. Selection is vendor-neutral: "gpu" runs on whichever GPU the
phsolver binary was built for, so switching hardware is a matter of
pointing $PHOTONHUB_SOLVER at the matching build. GPU↔CPU equivalence
follows per-feature tolerances (ModeSource included), not a
blanket bit-exact guarantee. Hardware records are dated snapshots: the then-current
suite passed 29/29 on a workstation GPU on 2026-06-27 and 47/47 on a
data-center GPU on 2026-07-12. Those counts do not attest a newer source
inventory; current refresh status is tracked in the internal GPU
verification records. CI compiles both GPU paths; hardware equivalence needs
the GPU-equivalence workflow or a GPU box.
cancel_event is an optional threading.Event; setting it
terminates the solver subprocess and raises SolverRunError. It is
used by interactive callers such as the desktop Stop button.
A simulation that fails Simulation.check_runnable is refused
before anything starts, a source inside the PML or absorber layers
included. allow_sources_in_boundary=True runs such a source anyway
and warns once per logical source instead: the recorded spectra are
physically meaningless, so it is for boundary diagnostics only.
submit
¶
submit(
sim: Simulation,
output_dir: Union[str, Path, None] = None,
solver_path: Union[str, Path, None] = None,
progress: Optional[Callable[[dict], None]] = None,
timeout: Optional[float] = None,
name: Optional[str] = None,
log_file: Union[str, Path, None] = None,
device: Optional[str] = None,
cancel_event: Optional[Event] = None,
) -> Job
Start sim on a background thread and return a Job handle
immediately; collect the result with job.result().
Takes run_local's arguments (log_file mirrors the engine event
stream to disk, device selects cpu/gpu backends) EXCEPT quiet: a
background job never draws the live status line (it would fight foreground
output and other jobs), pass progress to consume events instead.
cancel_event (or Job.cancel) terminates the solver early;
the job then raises SolverRunError from result().
Solver discovery¶
find_solver
¶
Locate the phsolver binary.
Resolution order is an explicit argument, $PHOTONHUB_SOLVER, a solver
recorded by photonhub install-solver or photonhub link-solver,
PATH, then the in-repository build directory. An explicit argument or
environment override that does not exist is an error, not a fallthrough.
Returns None only when nothing is configured and no binary is found.
A recorded install beats PATH because recording one is a deliberate
act, and someone who has run the installer should not be silently served a
different binary that happens to be earlier on the path. The environment
variable still beats both, so a one-off override needs no uninstall.
With PHOTONHUB_REQUIRE_SOURCE_MATCH=1 a recorded install is used only
when its info reports the revision this source tree has checked out,
the same test the in-repository build gets. The record lives in the
user's cache, so to a test run it is discovered, not chosen, and a released
binary there must not stand in for the build under test. Skipped, the
lookup carries on to PATH and the in-repository build.
SolverRunError
¶
Batches¶
Batch
¶
Batch(simulations: Mapping[str, Simulation])
A named collection of simulations run together. Keys are stable across the local and (future) cloud backends and become output subdirectory names, so they must be filesystem-safe.
quote
¶
Per-name CostEstimate plus the batch total
(the plan's per-batch upfront estimate). Returns
(per_sim: dict, total_usd: float).
run
¶
run(
path_dir: Union[str, Path, None] = None,
solver_path: Union[str, Path, None] = None,
max_workers: int = 1,
progress: Optional[Callable[[str, dict], None]] = None,
timeout: Optional[float] = None,
device: Optional[str] = None,
on_complete: Optional[
Callable[
[
str,
Optional[RunResult],
Optional[SolverRunError],
],
None,
]
] = None,
) -> BatchResults
Run every simulation, writing <path_dir>/<name>/ per entry, and
block until all finish. max_workers runs that many concurrently
(default 1 = serial). progress (if given) receives (name,
event) for each solver event; the per-run live status line is
always suppressed (concurrent entries would interleave).
device selects the backend for every entry, as in
run_local. Per-simulation failures are captured in
the returned BatchResults.errors, not raised. on_complete
receives (name, result, error) once per entry on the calling thread,
after its result or error is stored. Callback exceptions propagate.
Queued entries are cancelled; runs already started finish first.
BatchResults
¶
BatchResults(
results: Dict[str, RunResult],
errors: Dict[str, Exception],
path: Path,
names: List[str],
)
Results of a Batch.run. Dict-like over the successful runs
(batch_data[name] / items() / iteration), with failures captured in
errors. Local failures are SolverRunError; a remote batch
may also retain a resumable timeout carrying its service job id, or a
SimulationMismatchError when a job's saved
simulation is not the one submitted. Indexing a failed name re-raises that
exception; indexing an unknown name raises KeyError.
Job
¶
Job(
fn: Callable[[], RunResult],
name: Optional[str] = None,
job_id: Optional[str] = None,
cancel_event: Optional[Event] = None,
on_cancel: Optional[
Callable[[], Tuple[object, bool]]
] = None,
)
Handle to a single asynchronous local run (the forward-compatible shape
a cloud job handle will also satisfy). Created by submit; the
work runs on a daemon thread so the call returns immediately.
cancel
¶
Ask the solver to stop. No-op when the run has already finished or
the job was created without a cancel event; a cancelled run raises
SolverRunError from result.
A cloud job (ph.cloud.submit, ph.cloud.resume) is cancelled by
asking the service, which is what stops its spend, and this returns
the service's reply. When the reply says the job is cancelled the
local wait ends with the same SolverRunError; a job that
finished just before is still collected. It still reaches the service
after result gave up waiting (the job keeps running then),
asks only once however often it is called, and does nothing once the
result has been returned or the service reported the job finished. A
service that refuses raises its CloudError, and the job keeps
running.
Spec identity¶
spec_sha256
¶
Return the SHA-256 digest of the canonical wire document.
The digest covers sim.to_wire_json(indent=0). A cloud service that
stores this document unchanged records the same input digest. A submission
that omits an unsupported optional field, such as run.dft_shutoff on
the GPU, stores a different document. Identical wire inputs have identical
digests. Client-only
authoring state, including declared ports and frame origin, is not part of
the wire document and is not hashed. sim may be a Simulation or its
wire JSON as text or bytes.