Skip to content

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

find_solver(solver_path=None) -> Optional[Path]

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

SolverRunError(
    message: str,
    *,
    returncode: Optional[int] = None,
    stderr_tail: Optional[str] = None,
)

Bases: RuntimeError

phsolver could not be found, failed, or reported an error event.

returncode instance-attribute

returncode = returncode

stderr_tail instance-attribute

stderr_tail = stderr_tail

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.

simulations instance-attribute

simulations = validated

quote

quote(**kwargs)

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.

path instance-attribute

path = path

names property

names: List[str]

Every submitted name, in submission order (success or failure).

errors property

errors: Dict[str, Exception]

Per-name exceptions for failed or resumably timed-out runs.

succeeded property

succeeded: List[str]

Names of successful runs, in result insertion order.

failed property

failed: List[str]

Names of failed or timed-out runs, in error insertion order.

items

items() -> Iterator[Tuple[str, RunResult]]

Iterate (name, RunResult) over the successful runs only.

keys

keys() -> List[str]

Names of the successful runs.

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.

name instance-attribute

name = name

job_id instance-attribute

job_id = job_id

done property

done: bool

True once the run has finished (successfully or not).

cancel

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.

result

result(timeout: Optional[float] = None) -> RunResult

Block until the run finishes and return its RunResult, re-raising any SolverRunError in the caller's thread. Raises TimeoutError if timeout elapses first (the run keeps going; call again).

Spec identity

spec_sha256

spec_sha256(sim) -> str

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.