Qx.Hardware.Ibm (Qx - Quantum Computing Simulator v0.8.0)

View Source

HTTP client for IBM Quantum (Qiskit Runtime REST API).

Wraps Req; callers never touch HTTP details directly.

Auth

IBM Cloud splits identity into:

  • API key — exchanged at iam.cloud.ibm.com/identity/token for a 1-hour bearer token (access_token).
  • Service-CRN — sent on every API request to identify which Quantum instance the request is for. Cannot be derived from the API key.
  • Region — encoded into the CRN; the API base URL must match.

Tokens are 1-hour TTL; long queue waits routinely outlive them. Every authed call is wrapped in with_iam_refresh/2, which catches 401, runs a fresh IAM exchange once, and retries.

Sessions are optional — we don't use them

IBM's current spec (2026-05) treats sessions as optional and supports direct POST /jobs. Empirically verified against a production-proven reference (qx_server, last working 2026-02 and no relevant IBM API changes since per the changelog). Dropping sessions removes a request, an error path, and a leakage class.

Iron Law #1

IBM job-status values arrive as binaries from the wire (Pascal-Case per the documented enum: "Queued", "Running", "Completed", "Cancelled", "Cancelled - Ran too long", "Failed"). They are matched against @known_statuses and returned as binaries — never String.to_atom/1-ed.

Privacy invariant

This module never sees the qxportal token, and Qx.Hardware.Portal never sees the IBM API key or CRN. Two independent clients, two independent auth flows; the shared Qx.Hardware.Config struct is the only point of contact and each side reads only its own fields.

Summary

Functions

Returns the IBM Quantum API base URL for a region.

Cancels a running job. Returns :ok on 200/204 (including the "already terminal" idempotent path) and on 404 (job already gone).

Returns the coupling_map, basis_gates, and num_qubits for a backend. These are the fields qxportal's /api/v1/transpile payload requires.

Fetches the result of a finished Sampler job and aggregates the individual shot samples into a counts map.

Exchanges the user's API key for a 1-hour IAM bearer token.

Lists backends available on the user's instance.

Returns the current job status as a binary.

Submits a Sampler job. The PUB-format wrapping (pubs: [[qasm, nil, shots]]) is done here so callers never build the raw shape — forgetting the outer list is a 400. The 3-element PUB carries qasm + null observable + shot count (matches the production-proven qx_server wire format).

Functions

base_url_for(region)

@spec base_url_for(String.t()) :: String.t()

Returns the IBM Quantum API base URL for a region.

Known regions resolve to documented hosts; other allowlisted regions follow the standard <region>.quantum.cloud.ibm.com/api/v1 pattern.

cancel_job(config, job_id)

@spec cancel_job(Qx.Hardware.Config.t(), String.t()) :: :ok | {:error, term()}

Cancels a running job. Returns :ok on 200/204 (including the "already terminal" idempotent path) and on 404 (job already gone).

Uses POST /jobs/{id}/cancel per IBM's current spec.

fetch_backend_configuration(config, name)

@spec fetch_backend_configuration(Qx.Hardware.Config.t(), String.t()) ::
  {:ok,
   %{
     coupling_map: [[non_neg_integer()]],
     basis_gates: [String.t()],
     num_qubits: non_neg_integer()
   }}
  | {:error, term()}

Returns the coupling_map, basis_gates, and num_qubits for a backend. These are the fields qxportal's /api/v1/transpile payload requires.

IBM serves this static device shape from GET /v1/backends/{name}/configuration (NOT /properties, which returns time-varying per-gate / per-qubit calibration data). IBM names the qubit count n_qubits on the wire; we expose it as :num_qubits.

fetch_results(config, job_id)

@spec fetch_results(Qx.Hardware.Config.t(), String.t()) ::
  {:ok, %{counts: map(), metadata: map()}} | {:error, term()}

Fetches the result of a finished Sampler job and aggregates the individual shot samples into a counts map.

IBM's Sampler V2 response shape (verified live 2026-05):

{
  "results": [{
    "data": {
      "<classical_register_name>": {
        "samples": ["0x0", "0x3", "0x3", ...],
        "num_bits": 2
      }
    },
    "metadata": {...}
  }],
  "metadata": {...}
}

IBM does NOT pre-aggregate counts — each shot is returned as a hex bitstring under data.<reg>.samples. We aggregate ourselves: hex → integer → fixed-width binary → frequency map.

Returns the IBM-side merged metadata for downstream display.

Errors:

  • :unexpected_response — body doesn't have results: [_ | _]

  • :unsupported_result — first result has no recognizable data shape (e.g. Estimator tensor format)

iam_exchange(config)

@spec iam_exchange(Qx.Hardware.Config.t()) ::
  {:ok, Qx.Hardware.Config.t()} | {:error, term()}

Exchanges the user's API key for a 1-hour IAM bearer token.

Returns the input config with :access_token and :token_expires_at populated.

list_backends(config)

@spec list_backends(Qx.Hardware.Config.t()) ::
  {:ok,
   [%{name: String.t(), status: String.t() | nil, num_qubits: integer() | nil}]}
  | {:error, term()}

Lists backends available on the user's instance.

Decodes only :name, :status, :num_qubits from each entry.

poll_job(config, job_id)

@spec poll_job(Qx.Hardware.Config.t(), String.t()) ::
  {:ok, %{status: String.t(), reason: String.t() | nil}} | {:error, term()}

Returns the current job status as a binary.

IBM's GET /jobs/{id} returns BOTH a top-level status and a nested state.status (the schema-required path). We read state.status and fall back to top-level.

Status is matched against @known_statuses; an unknown value becomes {:error, {:unknown_status, raw}} so an API drift surfaces loudly rather than silently being misclassified.

submit_sampler(config, qasm, backend, shots)

@spec submit_sampler(Qx.Hardware.Config.t(), String.t(), String.t(), pos_integer()) ::
  {:ok, String.t()} | {:error, term()}

Submits a Sampler job. The PUB-format wrapping (pubs: [[qasm, nil, shots]]) is done here so callers never build the raw shape — forgetting the outer list is a 400. The 3-element PUB carries qasm + null observable + shot count (matches the production-proven qx_server wire format).

No session is opened.