QUAFU SDK (Python)
Run quantum circuits on the real devices and simulators of the QUAFU quantum computing platform from Python, and fetch the results.
- Install:
uv pip install quafu-sdk(orpip install quafu-sdk; in a Jupyter notebook:%pip install quafu-sdk) - Import:
import quafu(Qiskit backend adapter:from quafu.qiskit import QuafuProvider) - Command line:
quafu login/quafu logout/quafu whoami/quafu config - Python 3.9 or later; no third-party runtime dependencies. To use Qiskit circuits as input, install
pip install "quafu-sdk[qiskit]".
Quick start
import quafu
bell = """OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
cx q[0],q[1];
measure q[0] -> c[0];
measure q[1] -> c[1];
"""
job = quafu.submit(bell, target="sim", shots=1024)
print(job.result().counts) # e.g. {'00': 517, '11': 507}
On first run, the SDK prints a login link and tries to open your browser. Log in to QUAFU on the web page and click "Authorize"; the program then continues automatically.
Log in
The SDK supports two ways to log in.
| Browser login | API key | |
|---|---|---|
| How to get it | quafu.login() or quafu login, then authorize in the browser |
Create one on the website account page |
| Validity | 90 days from authorization; log in again when it expires (no renewal) | Never expires until disabled or deleted on the website |
| Best for | Personal computers, everyday use | Servers, scheduled jobs, CI, shared environments |
| Allowed operations | Submit, query, and cancel your own tasks | Same |
Neither method can manage your account or API keys; that can only be done on the website.
Browser login
quafu.login() # returns immediately if already logged in with more than 7 days left
quafu.login(force=True) # log in again (switch account or renew early)
quafu.login(open_browser=False) # only print the link; don't open a browser
quafu login # same as quafu.login()
quafu login --force
quafu login --no-browser
During login the SDK waits for either of two completion paths; whichever finishes first wins:
- Local browser: open the link on this machine and authorize; the browser redirects back to the SDK automatically, nothing else to do. Under WSL the link opens in your Windows default browser.
- Paste an authorization code: in environments that can't redirect back to the local machine (SSH terminals, containers, remote Jupyter), the web page shows an authorization code after you authorize. Copy it, paste it into the terminal or notebook input box, and press Enter.
The wait lasts 10 minutes. When you are not logged in, the first call that needs a login (such as submit() or whoami()) starts a browser login automatically; in non-interactive environments (CI, background jobs, pipes) it raises NotLoggedIn right away instead of waiting for input.
Within 7 days of expiry, the SDK prints a reminder to standard error once per run. After expiry, run quafu login again; for long-running scripts, use an API key.
Example of a successful login:
Logged in as 甲 (u7f3a9c2b) browser login expires: 2027-01-06
API key
Create an API key (starting with qf_) on the website account page, then use any of the following:
export QUAFU_API_KEY=qf_xxxxxxxx # environment variable; suited to servers and CI
c = quafu.Client(api_key="qf_xxxxxxxx") # pass explicitly; you can hold several Clients with different identities
quafu.login(api_key="qf_xxxxxxxx") # save on this machine (verified with the platform first; not saved if verification fails)
echo "$KEY" | quafu login --api-key - # read from standard input on the command line and save
Which credential is used
The first one found, in this order, is used; they are never merged:
- Passed explicitly:
quafu.Client(api_key=...) - Environment variable
QUAFU_API_KEY - Saved login (from browser login or a key saved with
login(api_key=...)) - None of the above: start a browser login if interactive, otherwise raise
NotLoggedIn
quafu.whoami() or quafu whoami shows the current account, credential kind (api_key / oauth), source (argument / env / saved), and expiry time. Credentials are only shown masked (e.g. qf_AbCdEfGh...) and never appear in logs or error messages.
Log out
quafu.logout()
quafu logout
For a browser login, the SDK first revokes the login on the platform, then deletes the saved login. The saved login is deleted even if the revoke request fails; in that case, revoke it under "Signed-in devices" on the website. If the saved credential is an API key, only the local copy is deleted and the key itself stays valid; to invalidate it, disable or delete it on the website account page.
Saved location
The saved login is stored in ~/.quafu-sdk/credentials.json (directory mode 0700, file mode 0600). Set the environment variable QUAFU_CONFIG_DIR to use another directory. One login is saved per platform URL.
Shared environments: every program running under the same system account can read this file. On servers or Jupyter kernels where several people share an account, use the QUAFU_API_KEY environment variable instead, and disable the key on the website when you are done.
Display language
CLI messages, login prompts, and exception messages are in English by default. Chinese is also available. The SDK never switches language based on the system locale.
Ways to set it, highest precedence first:
- In Python:
quafu.set_language('zh'). Applies to the current process only and is not saved;quafu.set_language(None)clears it.quafu.get_language()returns the effective language. - Environment variable
QUAFU_LANG=zh(oren). Forms such aszh_CN,zh-CN,en_US, andzh_CN.UTF-8are also accepted. Unrecognized values are ignored. - Saved setting:
quafu config set language zh/quafu config get language. Stored in~/.quafu-sdk/config.json(same directory as the credentials, honorsQUAFU_CONFIG_DIR), separate from credentials and shared across platform URLs. - Default:
en.
First login: the browser-login prompt ends with a line in the other language. In English mode it is 使用中文请按 Z 并回车。; in Chinese mode it is Press E then Enter to set English as display language. Typing just Z (or E), case-insensitive, and pressing Enter switches the language, saves it the same way as quafu config set language, reprints the prompt in the new language, and keeps waiting. On Windows a single keypress is enough when nothing else has been typed. Authorization codes are never a single letter, so this doesn't conflict with pasting a code. The hint line appears only while no language has been chosen (no environment variable, no saved setting) and only in interactive sessions; the keys work in any interactive login.
Submit circuits
job = quafu.submit(
circuits, # OpenQASM 2.0 text or qiskit.QuantumCircuit, or a list of them
target="sim", # where to run; see the table below
shots=1024, # number of runs per circuit
compiler="qiskit", # compilation mode; see the table below
name=None, # optional job name, shown under "My jobs" on the website
)
Each submit() creates one job; each circuit in the list is one task in the job, in submission order. submit() returns immediately without waiting for the run to finish.
target
| Value | Meaning |
|---|---|
"sim" |
Ideal simulator |
"<device>", e.g. "Dongling" |
The named real device; never moved to another device |
"<device>-sim", e.g. "Dongling-sim" |
Noisy simulation based on that device's calibration data |
"all-race" |
Send to all online real devices at once and take the first result to finish |
"all-redispatch" |
Send to one real device first, automatically moving to the next if it is unavailable |
Query available names and their current status with quafu.devices():
for d in quafu.devices():
print(d["name"], d["kind"], d["status"], d["n_qubits"], d["queue"])
kind is real (real device) or sim (simulator); queue is the number of tasks queued ahead on a real device, or None if unknown. Querying devices does not require login.
shots: real devices require a multiple of 1024 (1024–8192). The platform checks this and raises PrecheckError if it is not met.
compiler
| Value | Meaning |
|---|---|
"qiskit" (default) |
The platform compiles the circuit for the target chip. Write a logical circuit and declare only the qubits you use |
"none" |
No compilation; runs as is. The circuit must already be a physical circuit for this chip, with qubit indices matching the chip's numbering |
The compiler and version actually used are echoed in the result's compiler field.
Qiskit circuits
from qiskit import QuantumCircuit
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])
job = quafu.submit(qc, target="sim")
The SDK uses Qiskit to export the circuit to OpenQASM 2.0 before submitting, and sorts the trailing measurement statements by classical bit in ascending order (required by real devices). Circuits with unbound parameters must be bound with assign_parameters before submission.
Fetch results
job.id # job ID; after the program exits, reattach with quafu.job(job.id)
job.status() # Queued / Running / Finished / PartiallyFinished / Failed / Cancelled
r = job.result(timeout=600) # single circuit: wait until done, return a Result
rs = job.results() # multiple circuits: return a list of Results in submission order
job.cancel() # cancel tasks that have not finished
job = quafu.job("u7f3a9c2b-261008-153000") # reattach by job ID (only your own jobs)
result()is only for jobs with a single circuit; it raisesJobFailedon failure andJobCancelledif cancelled.results()does not raise when individual circuits fail: failed ones havecountsset toNone, andstatusanderrorexplain why.- If the job doesn't finish within
timeoutseconds,Timeoutis raised; the job remains on the platform, and you can call again to keep waiting. Ctrl-C only stops waiting; it does not cancel the job. cancel()returns the platform's response:cancelled_nowis the number of tasks cancelled outright;cancel_requestedis the number already sent to a real device, for which waiting can only be stopped (quota already consumed is not refunded). Tasks whose results are being fetched are not cancelled and produce results as usual.
Result
| Field | Meaning |
|---|---|
counts |
Raw counts {bitstring: count}; None if unsuccessful |
bit_order |
Bit order; "c0_rightmost" means the rightmost character of the bitstring is classical bit c[0] |
n_clbits |
Number of classical bits |
shots_requested / shots_returned |
Shots requested / shots actually counted (normalize with the latter) |
probabilities() |
Probabilities normalized by shots_returned |
measurements |
[{cbit, physical_qubit, virtual_qubit}]: which physical qubit each classical bit measured |
counts_by_qubit([…]) / reorder(…) / expectation_z(…) |
Counts rearranged by physical qubit or classical bit, Z-string expectation values; see "Per-qubit data" |
executed_qasm |
The circuit actually run |
compiler |
Compiler used: {name, version, optimization_level, seed} |
calibration_id |
ID of the calibration data used for this run (None for ideal simulation) |
target / backend |
Submitted target / backend actually used (for all-race and similar, the real device it landed on) |
status / ok / error |
Status of this task; failure reason {category, stage, retryable, detail, vars} |
parameter_values |
Parameter values for this task (sweeps, iterations, parameterized circuits); None if there are none |
iteration / evaluation_index |
Round and evaluation index in an iterative job |
task_id / raw |
Task ID; full details returned by the platform |
Parameter sweeps
In a circuit template, write gate parameters as {{param_name}}, then provide a set of parameter points. The platform substitutes each point in turn; each point is one task in the job:
template = """OPENQASM 2.0;
include "qelib1.inc";
qreg q[1];
creg c[1];
rx({{theta}}) q[0];
measure q[0] -> c[0];
"""
job = quafu.sweep(template, parameters=[("theta", "rad")], points=[0.0, 0.5, 1.0], target="sim")
t = job.table()
t.column("theta") # [0.0, 0.5, 1.0]
t.expectation_z([0]) # <Z> at each point
parameters: name and unit of each parameter ("rad","ns", etc.;"1"for dimensionless). The order defines the column order of each row inpoints.points: table of parameter points, as a nested list or numpy array; with a single parameter, each row can be a single number.- Other arguments (
shots,compiler,name) are the same as forsubmit().
You can also substitute locally and submit a list of circuits, labeling each with its parameter values: quafu.submit([{"qasm": ..., "parameter_values": {"theta": 0.5}}, ...], target, parameters=[("theta", "rad")]). For Qiskit parameterized circuits, see "Qiskit" below.
Iterative jobs
Variational algorithms (VQE, QAOA, etc.) need to "look at this round's results before deciding the next round". Open an iterative job and append round by round:
job = quafu.open_job("sim", template=template, parameters=[("theta", "rad")], name="VQE")
theta = 0.1
for k in range(10):
job.append(points=[[theta]], iteration=k)
job.wait() # returns when no tasks are queued or running
z = job.results()[-1].expectation_z([0])
theta = theta - 0.1 * z # illustration: update the parameter from the result
job.close() # close when done; no more appends afterwards
append(points=...)substitutes into the template (the job must be opened withtemplateandparameters);append(circuits=[...])appends circuits directly (text, Qiskit circuits, or circuit items withparameter_values).iterationis the round number andevaluation_indexthe evaluation number within the round; both appear in the results.- Append requests carry an idempotency key, so automatic retries after a lost response never append twice. Appending to a closed job raises
JobClosed.
Result tables
job.table() returns a columnar table (ResultTable) with rows aligned in submission order, suited to sweeps and iterations:
| Column | Meaning |
|---|---|
seq / task_ids |
Sequence number / task ID |
points, column("param_name") |
Parameter values of each row |
status / counts / shots_returned |
Status, counts, and shots actually counted per row; counts is None for unsuccessful rows |
iterations / evaluation_index |
Round and evaluation index in an iterative job |
errors |
Reasons for unsuccessful rows |
t.probabilities(), t.reordered(order), and t.expectation_z(cbits) compute row by row; t.to_dict() or t.to_pandas() (requires pandas) converts to a table. job.table(wait=False) returns the current snapshot immediately, useful for inspecting progress during iterations.
Per-qubit data
Both Result and ResultTable carry a bit-order declaration (bit_order) and a measurement mapping (measurements), so conversions are direct:
r.reorder([1, 0]) # bitstring reads c[1], c[0] from left to right; listing only some bits gives a marginal distribution
r.reorder("c0_leftmost") # switch to c[0] on the far left
r.counts_by_qubit([108, 109])# select by physical qubit
r.expectation_z([0, 1]) # Z⊗Z expectation value (computed from raw counts only)
Functions of the same names are also available at the top level: quafu.reorder_counts, quafu.counts_by_qubit, quafu.probabilities, quafu.expectation_z.
Devices and calibration data
d = quafu.device("Dongling") # status, qubit count, queue length, current calibration ID
cal = d.calibration() # current calibration data
cal.qubit(5)["t1_us"] # T1 of qubit 5 (microseconds)
cal.coupler(5, 6)["cz_fidelity"] # CZ gate fidelity between qubits 5 and 6
cal.measured_qubit(5) # readout data measured by the platform
quafu.calibration(r.calibration_id) # the calibration data used by a given run (immutable, retrievable long-term)
Field names include units (e.g. t1_us); fidelities are decimals from 0 to 1; values not measured or not filled in are None. Querying devices and calibration data does not require login.
Usage statistics
quafu.stats(days=30) # your task count, shots, daily breakdown, quota and runtime over the last 30 days
Quotas are counted per backend (sim / noisy / real) per day, and reset at 23:00 Beijing time. Each backend has two numbers:
- Normal quota (
limit): tasks beyond it are still accepted and run, but at the lowest priority — they are dispatched only when nobody has a within-quota task waiting. No error is raised. - Daily hard cap (
hard_limit): only when you reach it is a submission rejected withQuotaError, until the next reset.
q = quafu.stats()["quota"]["real"]
q["used"], q["limit"], q["hard_limit"] # submitted today / normal quota / daily hard cap
q["remaining"] # tasks left within the normal quota (never below 0)
q["degraded"] # True: tasks you submit now run at the lowest priority
q["resets_at"] # next reset, e.g. "2026-10-10T23:00:00+08:00"
Qiskit
from qiskit import QuantumCircuit, transpile
from quafu.qiskit import QuafuProvider, sweep
backend = QuafuProvider().get_backend("sim") # "sim", "<device>-sim", or "<device>"
job = backend.run(transpile(qc, backend), shots=1024)
job.result().get_counts()
job = sweep(qc_with_parameters, points, "sim") # parameterized circuit: substituted point by point locally, submitted as one job
job.table().column("theta")
- The backend's gate set and topology come from the platform;
backend.run()submits all circuits as one job. When some circuits fail, the failed experiments havesuccess=False. sweep()keeps the parameter names from Qiskit; the unit defaults to"rad".- Requires
pip install "quafu-sdk[qiskit]".
Platform URL
By default the SDK connects to https://quafu.com.cn. To connect to another deployment:
c = quafu.Client(base_url="https://example.org")
export QUAFU_BASE_URL=https://example.org
quafu --base-url https://example.org whoami
Precedence: argument > environment variable QUAFU_BASE_URL > default URL. Give the host only (no path), and it must be https, except for local loopback addresses (http://127.0.0.1:<port>).
Network
- Certificates are always verified; there is no option to disable verification. For deployments with self-signed certificates, specify the CA certificate with
Client(ca_bundle="ca.pem")or the environment variableQUAFU_CA_BUNDLE. - System proxy settings are not used by default. To use a proxy, specify an HTTP proxy with
Client(proxy="host:port")or the environment variableQUAFU_PROXY. - Read requests are retried automatically on network errors or platform 5xx responses; submissions carry an idempotency key, so retries after a lost response never submit twice.
Errors
All exceptions inherit from quafu.QuafuError; error messages explain the cause and what to do.
| Exception | When it occurs |
|---|---|
NotLoggedIn |
No credentials, and the environment is not interactive |
LoginFailed |
Browser login did not complete: authorization denied, timed out, or invalid authorization code |
LoginExpired |
The saved browser login has expired |
LoginRevoked |
The platform rejects the credential: the login was revoked, or the API key was disabled or deleted |
PrecheckError |
The platform rejected the circuit or parameters; category and vars give the specific reason |
QuotaError |
Daily hard cap or another limit reached; retry_after is the suggested wait in seconds, resets_at the next quota reset, vars the details. Going over the normal quota does not raise — those tasks just run at the lowest priority |
JobFailed / JobCancelled |
Circuit run failed / was cancelled; retryable indicates whether resubmitting unchanged makes sense |
JobNotFound |
The job does not exist or does not belong to the current account |
JobClosed |
Appending tasks to a closed or non-iterative job |
Timeout |
Waiting exceeded timeout |
ServerUnavailable |
Cannot connect to the platform, or the platform is temporarily unavailable |
FAQ
import quafu reports "another package occupying the quafu name", or quafu.login is missing after import
The current Python environment has another package installed that also uses the name quafu, and the two overwrite each other's files. Create a clean virtual environment with uv and install again. The error message shows only the commands for your system; each block below can be pasted as-is.
macOS / Linux:
uv venv quafu-env
source quafu-env/bin/activate
uv pip install quafu-sdk
Windows PowerShell (the second line lets this window run Activate.ps1):
uv venv quafu-env
Set-ExecutionPolicy Bypass -Scope Process -Force
quafu-env\Scripts\Activate.ps1
uv pip install quafu-sdk
Windows cmd:
uv venv quafu-env
quafu-env\Scripts\activate.bat
uv pip install quafu-sdk
NotLoggedIn when running on a server or in CI
These environments are not interactive, so no browser login is started. Create an API key on the website account page and set the environment variable QUAFU_API_KEY.
The browser doesn't redirect back after browser login
This is expected in remote terminals, containers, or remote Jupyter. Copy the authorization code shown on the web page, paste it into the terminal or notebook input box, and press Enter.
License
MIT
Metadata
Release files for quafu-sdk 0.2.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| quafu_sdk-0.2.3.tar.gz | 79.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| quafu_sdk-0.2.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 170.7 kB
Release files / quafu_sdk-0.2.3.tar.gz
| Download URL | quafu_sdk-0.2.3.tar.gz |
|---|---|
| Size | 79.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9366d2a434615b56606b3a35e96bef990d3bd19dfd5f509c6d1c602ac2121de7
|
|
BLAKE2b-256 checksum How to use checksums |
9ce0a705ba64387a4e9a105c0e9b958db4c78fa593502f9c0d541ed206c60d46
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / quafu_sdk-0.2.3-py3-none-any.whl
| Download URL | quafu_sdk-0.2.3-py3-none-any.whl |
|---|---|
| Size | 91.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8fb180b04dce0ab9764ce4d1358d012044cfcfe7c79b4e909281fce03977b573
|
|
BLAKE2b-256 checksum How to use checksums |
4eea577cffecb07e84c9b37c4c9a6c01310b7a80e6c9eaafe35e87a5483a19e8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|