# Kino.Qx
[Livebook](https://livebook.dev) Smart Cells + pipeline functions for
running quantum circuits on real IBM hardware via the
[Qx Portal](https://qxportal.dev).
Two cells ship in this package:
1. **Qx Snippet** — browse the snippets you've saved on the portal,
pick one from a dropdown, and inject the OpenQASM 3.0 (or converted
Elixir/Qx) source straight into a notebook cell.
2. **Qx Credentials** *(new in 0.2.0)* — collects portal URL, region,
backend, optimization level, and shots; emits a
`%Qx.Hardware.Config{}` binding (`qx`) that downstream cells pipe
circuits through `Kino.Qx.run!/2`.
End-to-end pipeline:
```elixir
circuit
|> Kino.Qx.run!(qx)
|> Qx.Draw.plot_counts(title: "Bell state")
```
## Status
`v0.2.0` is a **breaking architectural reset**. The previous
`Kino.Qx.TranspileCell` (paste-QASM + Submit button + inline result
rendering) is replaced by `Kino.Qx.CredentialsCell` + a `Kino.Qx.run!/2`
pipeline function. The transpile / submit / poll core moved upstream
into `Qx.Hardware` in the `:qx` library (0.7.0); `kino_qx` becomes a
thin UX layer that adds a live `Kino.Frame` status panel.
The portal-side JSON API is locked at `/api/v1` — see the
[API reference](https://qxportal.dev/api/v1/docs). The IBM Quantum
REST API is documented at
[quantum.cloud.ibm.com](https://quantum.cloud.ibm.com/docs/en/api/qiskit-runtime-rest).
## Installation
In a Livebook setup cell:
```elixir
Mix.install([
{:kino_qx, "~> 0.2"},
{:qx, "~> 0.7"}
])
```
Click **+ Smart** at the bottom of any notebook cell. You'll see both
cells in the menu — pick the one you need.
## Cell 1 — Qx Snippet (unchanged from 0.1)
Needs:
1. **A `qx_live_*` portal token.** *Dashboard → API Keys → Generate Key.*
2. **Portal URL** (defaults to `https://qxportal.dev`).
The token is stored only in transient cell state and **never**
serialized into the `.livemd` file.
## Cell 2 — Qx Credentials + `Kino.Qx.run!/2`
### Livebook secrets (one-time per notebook)
The cell **never asks for tokens directly**. It reads three Livebook
secrets at Connect / run time:
| Secret name | What it is |
|--------------------|----------------------------------------------------------------------------|
| `LB_PORTAL_TOKEN` | qxportal `qx_live_…` bearer (Dashboard → API Keys → Generate Key) |
| `LB_IBM_API_KEY` | IBM Cloud API key (<https://quantum-computing.ibm.com/> → Account) |
| `LB_IBM_CRN` | IBM Service-CRN (`crn:v1:bluemix:public:quantum:…`) |
Add each via the lock icon in Livebook's left sidebar before clicking
Connect. The `.livemd` file never carries any token — sharing a
notebook leaks nothing.
### Cell UI
* **Portal URL** — defaults to `https://test.qxquantum.com`. Validated
against a host allowlist (`*.qxquantum.com` over https;
`localhost`/`127.0.0.1` over http for dev) so a malicious shared
notebook cannot redirect your token.
* **Region** — `us-south` (default) or `eu-de`. Must match the region
encoded in your CRN.
* **Connect** — reads the three secrets, validates auth, populates the
backend dropdown.
* **Backend / Optimization / Shots** — chosen per cell run.
### Pipeline
```elixir
circuit =
Qx.create_circuit(2, 2)
|> Qx.h(0)
|> Qx.cx(0, 1)
|> Qx.measure(0, 0)
|> Qx.measure(1, 1)
circuit
|> Kino.Qx.run!(qx)
|> Qx.Draw.plot_counts(title: "Bell state")
```
A live `Kino.Frame` status panel renders above the result while the
job moves through transpile → submit → queued → running → done. If
you click Livebook's "Stop" button mid-run, a best-effort
`Qx.Hardware.cancel/3` fires for the in-flight job.
`Kino.Qx.run!/2` raises `Kino.Qx.RunError` on failure (pipe-friendly).
The tuple-returning `Kino.Qx.run/2,3` is also available for
production-style `with` chains.
### Outside Livebook
The hardware-execution core lives in `Qx.Hardware`. CLI scripts,
Phoenix apps, and OTP services can run circuits without a Livebook
runtime:
```elixir
config = %Qx.Hardware.Config{
portal_url: "https://test.qxquantum.com",
portal_token: System.fetch_env!("PORTAL_TOKEN"),
ibm_api_key: System.fetch_env!("IBM_API_KEY"),
ibm_crn: System.fetch_env!("IBM_CRN"),
ibm_region: "us-south",
backend: "ibm_brisbane"
}
{:ok, result} = Qx.Hardware.run(circuit, config)
```
`Kino.Qx.run/2,3` adds the status frame and cancel watcher around the
same `Qx.Hardware.run/3` call.
### What's NOT in v1
* **Estimator primitive** — Sampler only (Estimator deferred; base64
tensor decoding deserves its own iteration).
* **Multi-circuit batches** — one circuit per `run!/2` call.
* **OpenQASM 2.0 input** — qxportal accepts 3.0 only; convert
client-side first.
## Compatibility
| `kino_qx` | `qx` | Kino | Elixir |
|----------:|---------|---------|----------|
| 0.2.x | ~> 0.7 | ~> 0.19 | >= 1.17 |
| 0.1.x | n/a | ~> 0.19 | >= 1.17 |
## Troubleshooting
### Qx Snippet
| Symptom | Likely cause |
|----------------------|---------------------------------------------------------------|
| `unauthorized` (401) | Token is wrong, revoked, or for a different portal |
| Empty dropdown | You haven't saved any snippets to the portal yet |
| `rate_limited` (429) | More than 60 requests per minute on this key — wait + retry |
| Network timeout | Wrong portal URL, or the portal is unreachable from this host |
### Qx Credentials + `Kino.Qx.run!`
| Symptom | Likely cause |
|--------------------------------------------------|--------------|
| `Missing Livebook secret LB_PORTAL_TOKEN` | Add the secret via the lock icon in Livebook's sidebar |
| `Auth rejected (401)` from Connect | Secret values wrong, or IBM region doesn't match the CRN |
| `Qx.Hardware.NoMeasurementsError` | Circuit has no `Qx.measure/3` calls — Sampler V2 requires them |
| `Portal rejected the QASM (422)` | OpenQASM 2.0 input or syntax error; convert to 3.0 |
| `Portal transpile failed (502)` | Circuit too wide for the backend; try a smaller backend or `optimization_level: 0` |
| `Kino.Qx.Interrupted` | You clicked Livebook's Stop button mid-run; cancel was issued |
| Cell stuck on "queued" | IBM queue can be very long; the pipeline polls until terminal status |
| `Connect` succeeds but no backends | Your IBM instance has no backends provisioned for this region |
| `Portal URL must be https://…` | The URL is not on the allowlist (`*.qxquantum.com` over https) |
## License
[Apache 2.0](LICENSE).