Cloud client (ph.cloud)¶
The client for invitation-gated remote GPU execution. Configure credentials, preflight and price a job, run it, and read the same RunResult back. See the cloud guide.
Configuration¶
configure
¶
configure(
api_key: Optional[str] = None,
url: Optional[str] = None,
*,
cache_dir=None,
poll_interval_s: float = 2.0,
poll_backoff_max_s: float = 15.0,
request_timeout_s: float = 30.0,
allow_insecure_http: bool = False,
max_bundle_download_bytes: int = DEFAULT_MAX_COMPRESSED_BYTES,
max_bundle_extract_bytes: int = DEFAULT_MAX_EXPANDED_BYTES,
max_bundle_members: int = DEFAULT_MAX_MEMBERS,
) -> CloudConfig
Set the active cloud configuration. Returns it for inspection.
get_config
¶
get_config() -> CloudConfig
The active config, building one from the environment on first use.
CloudConfig
dataclass
¶
CloudConfig(
url: str,
api_key: str,
cache_dir: Path,
poll_interval_s: float = 2.0,
poll_backoff_max_s: float = 15.0,
request_timeout_s: float = 30.0,
allow_insecure_http: bool = False,
max_bundle_download_bytes: int = DEFAULT_MAX_COMPRESSED_BYTES,
max_bundle_extract_bytes: int = DEFAULT_MAX_EXPANDED_BYTES,
max_bundle_members: int = DEFAULT_MAX_MEMBERS,
)
Preflight and pricing¶
preflight
¶
preflight(
sim,
*,
device: str = "gpu",
solver=None,
max_usd: float = DEFAULT_MAX_USD,
) -> CloudPreflight
Get a device/solver-bound quote and enforce the spend/balance limit.
This call does not submit a job. max_usd defaults to the beta's $5
credit ceiling. The account must report available_usd (or exact
available_micros); falling back to total balance would be unsafe because
funds reserved by active jobs are not spendable. When the quote reports
cells_per_axis and dt_s, they must match the grid sim realizes
locally, before any price check; a quote that omits them warns. This
checks the quoted grid only: the solver image that runs the job is
versioned separately from the service that quotes it.
device and solver default as in run;
the returned device and solver are the resolved values the quote
is bound to, to pass with its quote_id.
CloudPreflight
dataclass
¶
CloudPreflight(
device: str,
solver: str | None,
max_usd: float,
quote_usd: float,
available_usd: float,
remaining_usd: float,
quote_id: str,
quote: dict[str, Any],
)
A quote that passed the caller's ceiling and available-balance checks.
quote_id is intentionally omitted from repr so displaying this
object in a notebook does not publish the opaque accepted-quote token.
quote retains the service response for audit fields such as cell count,
step count, rate, and expiry.
summary
¶
Format the accepted quote, spend limit, and available-balance checks.
Include expiry when the service supplied it. The quote identifier is omitted. This method makes no request and reserves no funds.
estimate
¶
Server-side quote bound to sim, device, and solver ref.
As in preflight, a quote whose cells_per_axis or dt_s
differ from the grid sim realizes locally raises CloudError
instead of returning its price, and a quote that omits them warns. The
check belongs here because the quote_id can be passed to
run or submit, which
receive only the id and so cannot check the grid themselves. It covers
the quoted grid only: the solver image that runs the job is versioned
separately from the service that quotes it.
device and solver default as in run:
the service's only GPU, and on a GPU the commit of the locally installed
solver. A quote_id from here binds to a submission made with the same
arguments. Defaults resolve to the same values only while the service's
GPU menu and the locally installed solver are unchanged; if either
changed in between, the service refuses the submission because it no
longer matches the quote, at no charge. To be sure, pass the resolved
values, which preflight returns beside its quote_id.
gpus
¶
The curated menu of GPUs you can run on, each a dict like
{"id": ..., "vendor": ..., "arch": ..., "gpu_mem_gb": ...}. Pass an id to
ph.cloud.run(sim, device="gpu:<id>"); bare device="gpu" (the
default) uses the only entry when the menu lists exactly one, and is
refused client-side when it lists several. The platform manages
which providers back each entry, that stays an internal detail.
Run¶
run
¶
Cloud run entry points, the prime directive: ph.cloud.submit returns the
same Job as the local path, so
job = ph.cloud.submit(sim); data = job.result() reads identically whether
local or cloud, and a server-side failure surfaces as the same
SolverRunError.
Where the two differ, it is to keep the spend bounded: every submission is
quoted first and refused above max_usd (default $5) unless the caller opts
out with max_usd=None; job.cancel() asks the service to cancel the
job; and wait_timeout_s bounds only the local wait, never the job.
CloudJobTimeout
¶
run
¶
run(
sim,
*,
max_usd: Optional[float] = DEFAULT_MAX_USD,
name=None,
device: Optional[str] = "gpu",
solver=None,
progress: ProgressCb = None,
wait_timeout_s: Optional[float] = None,
quote_id: Optional[str] = None,
) -> RunResult
Submit sim to the cloud under a dollar ceiling and block until its
result is ready. Returns a RunResult; raises
SolverRunError if the run fails, CloudError for
transport, auth or result-transfer problems and for a refused quote.
Spend-safe by default. The job is quoted first and submitted bound to
that quote only when it is within max_usd (default $5) and the
account's available balance, and when the quote's grid matches the one
sim realizes here; otherwise nothing is submitted (see
preflight). Pass the quote_id of a quote you
already accepted (from preflight or
estimate) to bind it as given, without quoting
again. max_usd=None is the explicit opt-out: an unquoted submission
with no ceiling.
wait_timeout_s bounds only how long this call waits. When it runs out
it raises CloudJobTimeout and the service job keeps running,
and billing: ask the service to cancel it with
ph.cloud.cancel(job_id) or collect it
with resume. timeout is its old name and warns. device
defaults to "gpu" as in every cloud call, which becomes the service's
GPU when gpus lists exactly one; "gpu:<id>"
picks one. On a GPU, solver defaults to the commit of the locally
installed solver (photonhub which-solver --info), which must be a
release solver reporting its full 40-character commit: a source build's
short id is refused before any request. Pass a commit to pin another
build. The service runs only builds it has approved for the GPU. The
quote and the submission carry the same device and solver. The check
covers the quoted grid only: the solver image that runs the job is
versioned separately from the service that quotes it.
run_quoted
¶
run_quoted(
sim,
*,
max_usd: float = DEFAULT_MAX_USD,
name=None,
device: str = "gpu",
solver=None,
progress: ProgressCb = None,
wait_timeout_s: Optional[float] = None,
) -> RunResult
Preflight and submit one quote-bound job under a hard dollar ceiling.
run does the same by default; this form has no unquoted opt-out
(max_usd must be a number) and takes no caller quote: it verifies a finite server quote, that the quote's
cells_per_axis and dt_s match the grid sim realizes locally
when the service reports them (a quote that omits them warns and is still
submitted), max_usd (default $5), and the account's available
balance before binding that quote id to the submission. A quote for a
different grid,
from a service whose SDK resolves the document differently, is refused
with CloudError; the solver image that runs the job is versioned
separately, so this checks the quote, not the run. The service remains
the final authority for quote expiry and concurrent balance changes.
device and solver default as in run.
submit
¶
submit(
sim,
*,
max_usd: Optional[float] = DEFAULT_MAX_USD,
name=None,
device: Optional[str] = "gpu",
solver=None,
progress: ProgressCb = None,
wait_timeout_s: Optional[float] = None,
quote_id: Optional[str] = None,
_preflighted_wire: Optional[dict] = None,
_defaults_resolved: bool = False,
) -> Job
Submit sim and return the same Job handle type as local
ph.submit once the service accepts the job. Polling and download
continue in the background; collect with job.result().
Spend-safe by default, exactly as run: quoted first and refused
above max_usd (default $5) or the available balance; a quote_id
you already accepted is bound as given; max_usd=None is the explicit
unquoted opt-out. device and solver default as in run.
job.cancel() asks the service to cancel the job and
returns its reply; a service that refuses raises CloudError, and the
job keeps running.
wait_timeout_s bounds only how long job.result() polls: the
service job keeps running, and billing, after it runs out.
timeout is its old name and warns.
submit_quoted
¶
submit_quoted(
sim,
*,
max_usd: float = DEFAULT_MAX_USD,
name=None,
device: str = "gpu",
solver=None,
progress: ProgressCb = None,
wait_timeout_s: Optional[float] = None,
) -> Job
Async form of run_quoted, returning the accepted service id.
resume
¶
resume(
job_id: str,
*,
simulation=None,
progress: ProgressCb = None,
wait_timeout_s: Optional[float] = None,
) -> Job
Return a handle that resumes an already-submitted service job.
If the job's result bundle was already downloaded and validated, the
handle loads it straight from the local cache, no service round-trip, so a finished run stays loadable offline and after the service ages the
job out. Otherwise it resumes polling exactly as before. job.cancel()
asks the service to cancel the job; wait_timeout_s bounds only the wait
(timeout is its old name and warns).
The result reads in the same absolute units and the same frame as the
call that submitted the job: this client stores the wire spec of every job
it submits, so the reader knows the simulation and undoes the engine's
per-unit-amplitude normalization here too, and it
stores beside it the client state the spec does not carry (the user-frame
origin of a fitted domain, the half a symmetry plane dropped, declared
ports), so coordinates, whole planes and transmission() agree. Both are stored
once the service returns the job id, so a job submitted from another
machine, through a cache that has since been cleared, or by a submission
whose response was lost has neither: pass simulation= then, and
whenever you still hold the simulation. It takes precedence over the
stored spec, and it must be the simulation that ran: one that differs from
the sim.json in the job's bundle, from the stored spec when the bundle
has none and that spec is the input the job recorded, or else from the grid
its manifest recorded, raises
SimulationMismatchError, and the cached result
stays. Passing nothing is never refused: where the job's
saved document differs from the stored spec, the job reads with what it
saved. With neither, the frequency-domain arrays carry the engine's
unit-amplitude values and say so in their normalization attr, and the
coordinates are the wire's corner frame.
submit
¶
submit(
sim,
*,
max_usd: Optional[float] = DEFAULT_MAX_USD,
name=None,
device: Optional[str] = "gpu",
solver=None,
progress: ProgressCb = None,
wait_timeout_s: Optional[float] = None,
quote_id: Optional[str] = None,
_preflighted_wire: Optional[dict] = None,
_defaults_resolved: bool = False,
) -> Job
Submit sim and return the same Job handle type as local
ph.submit once the service accepts the job. Polling and download
continue in the background; collect with job.result().
Spend-safe by default, exactly as run: quoted first and refused
above max_usd (default $5) or the available balance; a quote_id
you already accepted is bound as given; max_usd=None is the explicit
unquoted opt-out. device and solver default as in run.
job.cancel() asks the service to cancel the job and
returns its reply; a service that refuses raises CloudError, and the
job keeps running.
wait_timeout_s bounds only how long job.result() polls: the
service job keeps running, and billing, after it runs out.
timeout is its old name and warns.
run_quoted
¶
run_quoted(
sim,
*,
max_usd: float = DEFAULT_MAX_USD,
name=None,
device: str = "gpu",
solver=None,
progress: ProgressCb = None,
wait_timeout_s: Optional[float] = None,
) -> RunResult
Preflight and submit one quote-bound job under a hard dollar ceiling.
run does the same by default; this form has no unquoted opt-out
(max_usd must be a number) and takes no caller quote: it verifies a finite server quote, that the quote's
cells_per_axis and dt_s match the grid sim realizes locally
when the service reports them (a quote that omits them warns and is still
submitted), max_usd (default $5), and the account's available
balance before binding that quote id to the submission. A quote for a
different grid,
from a service whose SDK resolves the document differently, is refused
with CloudError; the solver image that runs the job is versioned
separately, so this checks the quote, not the run. The service remains
the final authority for quote expiry and concurrent balance changes.
device and solver default as in run.
submit_quoted
¶
submit_quoted(
sim,
*,
max_usd: float = DEFAULT_MAX_USD,
name=None,
device: str = "gpu",
solver=None,
progress: ProgressCb = None,
wait_timeout_s: Optional[float] = None,
) -> Job
Async form of run_quoted, returning the accepted service id.
resume
¶
resume(
job_id: str,
*,
simulation=None,
progress: ProgressCb = None,
wait_timeout_s: Optional[float] = None,
) -> Job
Return a handle that resumes an already-submitted service job.
If the job's result bundle was already downloaded and validated, the
handle loads it straight from the local cache, no service round-trip, so a finished run stays loadable offline and after the service ages the
job out. Otherwise it resumes polling exactly as before. job.cancel()
asks the service to cancel the job; wait_timeout_s bounds only the wait
(timeout is its old name and warns).
The result reads in the same absolute units and the same frame as the
call that submitted the job: this client stores the wire spec of every job
it submits, so the reader knows the simulation and undoes the engine's
per-unit-amplitude normalization here too, and it
stores beside it the client state the spec does not carry (the user-frame
origin of a fitted domain, the half a symmetry plane dropped, declared
ports), so coordinates, whole planes and transmission() agree. Both are stored
once the service returns the job id, so a job submitted from another
machine, through a cache that has since been cleared, or by a submission
whose response was lost has neither: pass simulation= then, and
whenever you still hold the simulation. It takes precedence over the
stored spec, and it must be the simulation that ran: one that differs from
the sim.json in the job's bundle, from the stored spec when the bundle
has none and that spec is the input the job recorded, or else from the grid
its manifest recorded, raises
SimulationMismatchError, and the cached result
stays. Passing nothing is never refused: where the job's
saved document differs from the stored spec, the job reads with what it
saved. With neither, the frequency-domain arrays carry the engine's
unit-amplitude values and say so in their normalization attr, and the
coordinates are the wire's corner frame.
Batch
¶
Batch(simulations: Mapping[str, Simulation])
Named simulations submitted as independent cloud jobs.
Construction validates names and simulation types and makes no requests.
quote returns local cost estimates. run validates all service quotes
and spend limits before submitting jobs in parallel. Successful results and
per-name failures share the local BatchResults interface.
run
¶
run(
*,
device="gpu",
solver=None,
max_workers: int = 4,
wait_timeout_s: Optional[float] = None,
max_usd=None,
max_usd_total=None,
on_submit=None,
on_complete=None,
) -> BatchResults
Quote every entry, then submit within its accepted spend limit.
max_usd is required and is either one per-job limit applied to all
entries or a mapping whose keys exactly match the batch. All estimates
are validated before any job is submitted, including, when the service
reports a quote's cells_per_axis and dt_s, that they match the
grid its simulation realizes locally (a quote that omits them warns
and is still submitted), and each accepted quote id is bound to its
corresponding submission. device and solver default as in
run (the service's only GPU, and on a GPU the
commit of the locally installed solver), resolved once for the whole
batch and sent unchanged on every estimate, refresh and submission.
wait_timeout_s bounds only how long each entry is
waited for: an entry past it is reported as a
CloudJobTimeout and its service job keeps
running, and billing (timeout is its old name and warns).
max_usd_total optionally caps the sum of accepted quotes. A quote
above either ceiling in the initial pass refuses the whole batch.
Batches larger than service concurrency run in waves: set
max_workers to the service's active-job limit (1 for the beta).
Immediately before submission, quotes near expiry are refreshed and
both ceilings checked again against accepted spend. The refresh margin
is one quarter of the remaining lifetime when the quote is received,
capped at 60 seconds and fixed for that quote.
A missing expires_at means no expiry. A refresh refusal fails only
that entry; earlier jobs may already have been charged.
on_submit(name, job_id, quote_usd) runs on a worker thread after
submission. on_complete(name, result, error) runs on the calling
thread as each entry finishes. Submit hooks have their own lock.
on_submit exceptions are reported as warnings, never raised,
because the job is already paid. on_complete exceptions propagate.
If collection is interrupted, queued entries are cancelled before the
exception is raised; entries already running are waited for first.
A submit-time CloudError carries its accepted quote_usd because
the service may have accepted a job before the response was lost.
Job control¶
list_jobs
¶
Recent service jobs, including normalized quote/actual/refund dollars.
The retention window and ordering are service policy. This is the recovery surface for finding a paid job id after a notebook or process exits.
job_status
¶
One service job's current state, progress, and cost metadata.
cancel
¶
Request cancellation of a cloud job and return the service response.
Validate job_id before the request. This does not wait for a terminal
state or determine the final charge or refund. Use job_status to inspect
the recorded state and costs.
Account¶
whoami
¶
Return the service identity associated with the configured API key.
This makes an authenticated request. Response-body or socket-read timeouts
can propagate as TimeoutError. Other transport and service errors raise
CloudError.
create_api_key
¶
Mint a new API key (the plaintext token is returned exactly once).
Transport and errors¶
HttpClient
¶
HttpClient(cfg: CloudConfig)
HTTP transport for the configured PhotonHub cloud service.
Requests use the API key and request timeout in cfg. GET requests retry
server and network errors with bounded backoff unless a caller asks for
one attempt. POST and DELETE requests are attempted once. HTTP errors
raise CloudError with parsed details in body and the service's
reason, one bounded line with secrets redacted, in the message.
Result redirects do not forward the bearer token to the redirected URL.
get_json
¶
GET a service-relative path and decode its JSON response.
Enforce the JSON body-size limit. Response-body or socket-read timeouts
can propagate as TimeoutError, even without a deadline. An optional
monotonic deadline bounds request opening and retry waits; expiry
also raises TimeoutError. retry=False makes one attempt only.
Other transport, body, or decoding failures raise CloudError.
post_json
¶
POST a JSON body once to a service-relative path and decode the response.
Enforce the JSON body-size limit. Transport, body, and decoding failures
raise CloudError. The request is not retried.
delete
¶
Send one DELETE request to a service-relative path and return its status.
HTTP errors raise CloudError. The request is not retried.
get_bytes
¶
Read a response with a hard byte ceiling.
This compatibility helper remains useful for small endpoints and test
transports. Large result bundles use get_to_file so they are
never duplicated in memory.
get_to_file
¶
Stream one GET response into dest with a hard byte ceiling.
Overwrite an existing destination. A rejected, truncated, or failed
transfer removes dest, even if it existed before the call or the
request failed before opening it. Use a new temporary destination to
retain a previous result. The parent directory must exist.
Returns the number of bytes written.
whoami
¶
Fetch the identity associated with the configured API key.
Response-body or socket-read timeouts can propagate as TimeoutError.
Other transport and service errors raise CloudError.
account
¶
Fetch the service account balance and usage response without normalization.
estimate
¶
Request a quote for a solver document and optional device and solver.
Return the raw service response. This transport method does not compare grids, enforce a spend limit, or submit a job.
create_api_key
¶
Request a new API key with name and return the service response.
Treat the returned plaintext token as a secret.
list_gpus
¶
Return the service's GPU list, or an empty list when its key is absent.
deadline and retry are as in get_json.
list_jobs
¶
Fetch recent job records from the service.
Raise CloudError if the response does not contain a list of dicts.
Ordering and retention depend on the service.
submit_job
¶
Submit a solver document with the supplied optional job metadata.
Return the service response. This low-level method does not perform
preflight or enforce a spend limit. Use ph.cloud.submit for the
checked submission path.
get_job
¶
Validate the job identifier and fetch its raw service status.
An optional monotonic deadline bounds request time and retry waits.
cancel_job
¶
Validate the job identifier and request cancellation.
Return the service response. This method does not poll for completion.
download_result
¶
Download a job's result bundle into memory under the configured byte limit.
Validate the job identifier. Oversized or truncated downloads raise
CloudError.
download_result_to
¶
Stream a job's result bundle to dest and return the bytes written.
Validate the job identifier and enforce max_bytes or the configured
download ceiling. Overwrite an existing destination. Failed transfers
remove dest, even if it existed before the call or the request failed
before opening it. Use a new temporary destination to retain a previous
result. The parent directory must exist.
CloudError
¶
CloudError(
message: str,
*,
status_code: Optional[int] = None,
body=None,
job_id: Optional[str] = None,
)
Bases: RuntimeError
A cloud client/transport/auth/result-transfer error, NOT a simulation
failure (that is SolverRunError). Accepted jobs expose job_id for
safe resume without a duplicate paid submission.