# Directly Using the QDMI Device Library to Run Quantum Workloads on IQM Hardware via QDMI-on-IQM

This guide demonstrates how to use the IQM QDMI Device library to communicate
with IQM’s quantum computing hardware.

## Configuring the QDMI Device

The QDMI device connects to the unified IQM Server API to communicate with IQM’s
quantum computing hardware.

### Authentication Methods

The QDMI device supports multiple authentication methods:

1. **Environment Variables** (recommended for ease of use):
   - `IQM_TOKEN`: Bearer token for authentication
   - `IQM_TOKENS_FILE`: Path to a file containing authentication tokens
2. **Explicit Parameters**: Authentication credentials can be set
   programmatically via session parameters.

For the environment variable setup used in the Python example scripts, see
[Configure Your Environment](examples.html.md#configure-your-environment) in the
Examples guide.

**Important**: Authentication credentials are resolved in the following order:

- If explicit parameters are set via `QDMI_DEVICE_SESSION_PARAMETER_TOKEN` or
  `QDMI_DEVICE_SESSION_PARAMETER_AUTHFILE`, they take precedence and the
  environment variables are ignored.
- If no explicit parameters are set, the device will automatically use the
  `IQM_TOKEN` or `IQM_TOKENS_FILE` environment variables if they are defined.
- If both environment variables and explicit parameters are set simultaneously,
  the explicit parameters will take precedence.

### TLS Certificates

Linux wheels use the host’s CA trust store, discovering the standard CA bundle
on Debian/Ubuntu, RHEL, SUSE, and Alpine systems at runtime. Install your
distribution’s `ca-certificates` package if it is missing. Other platforms keep
libcurl’s native defaults.

For a private CA or a nonstandard bundle location, set `CURL_CA_BUNDLE` to a PEM
bundle before making requests. `SSL_CERT_FILE` is also supported when
`CURL_CA_BUNDLE` is unset or empty. Invalid explicit paths cause requests to
fail; certificate and hostname verification remain enabled. This applies to both
native and Python clients.

### Session Configuration

The internal `QDMIClient` test helper configures a device session as shown
below. It calls the IQM QDMI Device interface directly and is not part of the
installed API. Each failed call throws; the helper frees the session on failure
and transfers ownership to the caller on success.

```cpp
auto QDMIClient::get_iqm_session(const std::string &base_url,
                                 const std::optional<std::string> &token,
                                 const std::optional<std::string> &tokens_file,
                                 const std::optional<std::string> &qc_id,
                                 const std::optional<std::string> &qc_alias)
    -> IQM_QDMI_Device_Session {
  IQM_QDMI_Device_Session raw_session = nullptr;
  auto ret = IQM_QDMI_device_session_alloc(&raw_session);
  throw_if_error(ret, "Failed to allocate IQM QDMI device session.");
  // Every step below reports failure by throwing, and the caller has no handle
  // to free until this function returns one.
  Owned_session session{raw_session};

  // Set the session parameters
  ret = IQM_QDMI_device_session_set_parameter(
      session.get(), QDMI_DEVICE_SESSION_PARAMETER_BASEURL, base_url.size() + 1,
      base_url.c_str());
  throw_if_error(ret, "Failed to set the base URL for the device session.");

  if (token.has_value()) {
    ret = IQM_QDMI_device_session_set_parameter(
        session.get(), QDMI_DEVICE_SESSION_PARAMETER_TOKEN, token->size() + 1,
        token->c_str());
    throw_if_error(ret, "Failed to set the token for the device session.");
  }

  if (tokens_file.has_value()) {
    ret = IQM_QDMI_device_session_set_parameter(
        session.get(), QDMI_DEVICE_SESSION_PARAMETER_AUTHFILE,
        tokens_file->size() + 1, tokens_file->c_str());
    throw_if_error(ret,
                   "Failed to set the tokens file for the device session.");
  }

  if (qc_id.has_value()) {
    ret = IQM_QDMI_device_session_set_parameter(
        session.get(), QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, qc_id->size() + 1,
        qc_id->c_str());
    throw_if_error(
        ret, "Failed to set the quantum computer ID for the device session.");
  }

  if (qc_alias.has_value()) {
    ret = IQM_QDMI_device_session_set_parameter(
        session.get(), QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2,
        qc_alias->size() + 1, qc_alias->c_str());
    throw_if_error(
        ret,
        "Failed to set the quantum computer alias for the device session.");
  }

  ret = IQM_QDMI_device_session_init(session.get());
  throw_if_error(ret, "Failed to initialize the device session.");

  return session.release();
}

```

Set optional session parameters, such as the HTTP request timeout, before
calling [`IQM_QDMI_device_session_init()`](capi/device.html.md#_CPPv428IQM_QDMI_device_session_init23IQM_QDMI_Device_Session).

The [`IQM_QDMI_device_session_alloc()`](capi/device.html.md#_CPPv429IQM_QDMI_device_session_allocP23IQM_QDMI_Device_Session) function allocates a new session
object, and the [`IQM_QDMI_device_session_set_parameter()`](capi/device.html.md#_CPPv437IQM_QDMI_device_session_set_parameter23IQM_QDMI_Device_Session29QDMI_Device_Session_Parameter6size_tPKv) function is
used to set various parameters for the session:

- **Base URL**
  ([`QDMI_DEVICE_SESSION_PARAMETER_BASEURL`](capi/constants.html.md#_CPPv4N31QDMI_DEVICE_SESSION_PARAMETER_T37QDMI_DEVICE_SESSION_PARAMETER_BASEURLE)):
  The URL of the IQM server.
- **Quantum Computer ID**
  ([`QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1`](capi/constants.html.md#_CPPv4N31QDMI_DEVICE_SESSION_PARAMETER_T37QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1E)):
  Optional ID of the specific quantum computer to use. If not set, falls back to
  `IQM_QC_ID`.
- **Quantum Computer Alias**
  ([`QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2`](capi/constants.html.md#_CPPv4N31QDMI_DEVICE_SESSION_PARAMETER_T37QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2E)):
  Optional alias of the specific quantum computer to use. If not set, falls back
  to `IQM_QC_ALIAS`.
- **HTTP Request Timeout**
  ([`QDMI_DEVICE_SESSION_PARAMETER_CUSTOM3`](capi/constants.html.md#_CPPv4N31QDMI_DEVICE_SESSION_PARAMETER_T37QDMI_DEVICE_SESSION_PARAMETER_CUSTOM3E)):
  Optional positive `uint64_t` duration in milliseconds applied to every HTTP
  request made by the session. If not set, each request uses the default
  one-hour timeout.
- **Calibration Set ID** (`QDMI_DEVICE_SESSION_PARAMETER_CUSTOM4`): Optional
  null-terminated canonical UUID string (lowercase hexadecimal). Set it before
  session initialization to select and pin the architecture, quality metrics,
  and execution calibration. Initialization fails if the server cannot supply
  the requested set.
- **Authentication Token**
  ([`QDMI_DEVICE_SESSION_PARAMETER_TOKEN`](capi/constants.html.md#_CPPv4N31QDMI_DEVICE_SESSION_PARAMETER_T35QDMI_DEVICE_SESSION_PARAMETER_TOKENE)):
  Bearer token for authentication. If not set, falls back to the `IQM_TOKEN`
  environment variable.
- **Tokens File**
  ([`QDMI_DEVICE_SESSION_PARAMETER_AUTHFILE`](capi/constants.html.md#_CPPv4N31QDMI_DEVICE_SESSION_PARAMETER_T38QDMI_DEVICE_SESSION_PARAMETER_AUTHFILEE)):
  Path to a file containing authentication tokens. If not set, falls back to the
  `IQM_TOKENS_FILE` environment variable.

**Note on Authentication**: If you set either authentication parameter
explicitly, the corresponding environment variable will be ignored. This allows
you to override environment-based authentication when needed.

If neither quantum computer ID nor alias is specified, the first available
quantum computer from the server will be used.

If the base URL is not specified explicitly, the session initialization path
falls back to `IQM_SERVER_URL`, then its `IQM_BASE_URL` alias, before using the
standard Resonance endpoint. A quantum computer alias similarly falls back to
`IQM_QUANTUM_COMPUTER`, then its `IQM_QC_ALIAS` alias. `IQM_QC_ID` remains a
separate quantum computer ID selector.

The session is initialized with [`IQM_QDMI_device_session_init()`](capi/device.html.md#_CPPv428IQM_QDMI_device_session_init23IQM_QDMI_Device_Session), which:

1. Fetches the list of available quantum computers from the server
2. Selects the appropriate quantum computer (by ID, alias, or first available)
3. Retrieves the static quantum architecture (qubits and connectivity)
4. Fetches the dynamic quantum architecture with calibrated gates for the
   default calibration set
5. Retrieves calibration metrics (T1/T2 times, gate fidelities) if available
6. Checks whether calibration jobs are supported by the server

At any time, `ret` is the [`QDMI_STATUS`](capi/constants.html.md#_CPPv411QDMI_STATUS) return value of the last
function call, which can be checked for error codes.

### Session Initialization Details

When [`IQM_QDMI_device_session_init()`](capi/device.html.md#_CPPv428IQM_QDMI_device_session_init23IQM_QDMI_Device_Session) is called, the following steps
occur:

1. **Authentication Setup**: The token manager is initialized with the provided
   authentication credentials.
2. **Quantum Computer Selection**:
   - The system fetches the list of available quantum computers from the server.
   - If a quantum computer ID was specified, it searches for that ID and
     retrieves the corresponding alias.
   - If a quantum computer alias was specified, it searches for that alias and
     retrieves the corresponding ID.
   - If neither was specified, the first available quantum computer is selected.
3. **Static Architecture Retrieval**:
   - The static quantum architecture is fetched, containing the qubits and their
     connectivity.
   - This information is stored in memory for efficient querying.
4. **Dynamic Architecture Retrieval**:
   - The dynamic quantum architecture is fetched for the “default” calibration
     set.
   - This includes the list of calibrated gates and their implementations.
   - The calibration set ID is stored for use in job submissions.
5. **Quality Metrics Retrieval**:
   - If available, calibration metrics are fetched from the server.
   - This includes T1 and T2 coherence times for qubits.
   - Gate fidelities for single-qubit (prx, measure) and two-qubit (cz)
     operations.
6. **Calibration Job Support Check**:
   - The system checks if the server supports calibration jobs by querying the
     COCOS health endpoint.
   - This determines whether the IQM calibration extension is available.

After initialization, the session is ready to submit jobs and query device
information.

For the REST API endpoints called during each of these steps, see
[IQM API Usage in QDMI Device Implementation](contributing.html.md#iqm-api-usage-in-qdmi-device-implementation)
in the Contributing guide.

## Using the Device with MQT Core

The installed CMake target identifies its device manifest through
`QDMI_MANIFEST_NAME`. The manifest contains stable device IDs, session defaults,
the symbol prefix and relative library path. An application using MQT Core can
copy the device library and manifest beside its executable. This integration
requires CMake 3.28 or later:

```cmake
# FIXME: Require mqt-core 4.1.0 once released; 4.0.0 does not copy the QDMI driver.
find_package(mqt-core 4.0.0 CONFIG REQUIRED)
find_package(iqm-qdmi-device CONFIG REQUIRED)

add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE MQT::CoreQDMI)
mqt_copy_qdmi_runtime(my_app iqm-qdmi-device)
```

The helper copies the MQT Core QDMI driver, device library, and manifest beside
the application. The driver resolves relative library paths from the manifest
directory.

Python consumers use installed entry-point metadata to discover the manifest
without importing provider code or loading the native library. The Python
package advertises its manifest with:

```toml
[project.entry-points]
"mqt.core.qdmi.manifests".iqm = "iqm.qdmi"
```

The catalogue defines the following Resonance connections:

| System   | Hardware stable ID   | Mock stable ID     |
|----------|----------------------|--------------------|
| Garnet   | `iqm.garnet`         | `iqm.garnet.mock`  |
| Emerald  | `iqm.emerald`        | `iqm.emerald.mock` |
| Sirius   | `iqm.sirius`         | `iqm.sirius.mock`  |

These definitions use `https://resonance.iqm.tech` and select the corresponding
alias, such as `emerald` or `emerald:mock`. Mocks also run on Resonance and use
its authentication. `iqm.default` remains available for custom connections and
environment-based selection.

List configured IDs offline, then open only the selected device:

```python
from mqt.core.qdmi import builtin_driver

print(builtin_driver.registered_device_ids())
device = builtin_driver.open_device("iqm.emerald.mock", token="…")
```

An explicit configuration augments built-in and installed device definitions and
overrides definitions with the same stable ID. See
[Python Package](python_package.html.md) for Qiskit integration and device queries.
An explicitly configured quantum computer ID or alias takes precedence over both
`IQM_QC_ID` and `IQM_QUANTUM_COMPUTER` environment defaults.

## Running Jobs via Slurm

For Slurm-backed native job submission, see the
[SPANK Plugin Guide](spank_plugin.html.md).

## Understanding Quantum Architecture and Calibration Sets

The IQM Server API distinguishes between two types of quantum architecture:

### Static Quantum Architecture

The **static quantum architecture** defines the physical layout of the quantum
computer:

- **Qubits**: The set of available qubits (e.g., “QB1”, “QB2”, “QB3”, etc.)
- **Connectivity**: The coupling map showing which qubits are connected and can
  interact

This information is fixed for a given quantum computer and fetched once during
session initialization.

### Dynamic Quantum Architecture

The **dynamic quantum architecture** defines the calibrated operations available
on the quantum computer:

- **Calibrated Gates**: The set of gates that are currently calibrated and ready
  to use
- **Gate Implementations**: The specific implementations of each gate (e.g.,
  “phased_rx” for prx)
- **Calibration Set ID**: A unique identifier for the current calibration data

The dynamic architecture is tied to a specific **calibration set**. Each time
the quantum computer is calibrated, a new calibration set is created with
updated gate implementations and quality metrics.

### Calibration Sets

A **calibration set** represents a snapshot of the quantum computer’s
calibration data at a specific point in time. It includes:

- The set of calibrated gates and their implementations
- Quality metrics for qubits (T1, T2 coherence times)
- Quality metrics for operations (gate fidelities)

Without a selector, session initialization resolves the server’s default
calibration set. Every session retains its resolved architecture and metrics for
its lifetime. A calibration job returns a new set ID; open a new session with
that ID before compiling and running circuits against the new set.

Each local circuit job uses the session calibration for submission. Query
`QDMI_DEVICE_JOB_PROPERTY_CUSTOM1` for this null-terminated UUID string. This
property is unavailable for retrieved remote jobs, whose original calibration is
not inferred from the retrieval session.

## Querying Device Information

The QDMI device allows you to query various information about the quantum
computing hardware, such as the available qubits, operations, and their
properties. Architecture and calibration information is fetched during session
initialization and kept in memory for efficient querying. Dynamic properties,
such as queue length, are fetched when queried.

The following properties about the device can be queried via the
[`IQM_QDMI_device_session_query_device_property()`](capi/device.html.md#_CPPv445IQM_QDMI_device_session_query_device_property23IQM_QDMI_Device_Session20QDMI_Device_Property6size_tPvP6size_t) function:

- [`QDMI_DEVICE_PROPERTY_NAME`](capi/constants.html.md#_CPPv4N22QDMI_DEVICE_PROPERTY_T25QDMI_DEVICE_PROPERTY_NAMEE): The
  name/alias of the quantum computer.
- [`QDMI_DEVICE_PROPERTY_VERSION`](capi/constants.html.md#_CPPv4N22QDMI_DEVICE_PROPERTY_T28QDMI_DEVICE_PROPERTY_VERSIONE): The
  version of the IQM QDMI device implementation.
- [`QDMI_DEVICE_PROPERTY_LIBRARYVERSION`](capi/constants.html.md#_CPPv4N22QDMI_DEVICE_PROPERTY_T35QDMI_DEVICE_PROPERTY_LIBRARYVERSIONE):
  The version of the QDMI library.
- [`QDMI_DEVICE_PROPERTY_STATUS`](capi/constants.html.md#_CPPv4N22QDMI_DEVICE_PROPERTY_T27QDMI_DEVICE_PROPERTY_STATUSE): The
  current status of the device (e.g., idle, busy). It is derived from what the
  IQM Server reports about the quantum computer at the time of the query — its
  health, the availability of its queue, and the number of queued jobs — so it
  accounts for jobs submitted outside of QDMI as well. A quantum computer that
  reports itself unhealthy, or whose queue has no availability, is reported as
  under maintenance.
- [`QDMI_DEVICE_PROPERTY_QUEUELENGTH`](capi/constants.html.md#_CPPv4N22QDMI_DEVICE_PROPERTY_T32QDMI_DEVICE_PROPERTY_QUEUELENGTHE):
  The current number of jobs waiting in the selected quantum computer’s queue,
  if the IQM backend exposes queue availability.
- [`QDMI_DEVICE_PROPERTY_QUBITSNUM`](capi/constants.html.md#_CPPv4N22QDMI_DEVICE_PROPERTY_T30QDMI_DEVICE_PROPERTY_QUBITSNUME): The
  number of qubits available on the device.
- [`QDMI_DEVICE_PROPERTY_SITES`](capi/constants.html.md#_CPPv4N22QDMI_DEVICE_PROPERTY_T26QDMI_DEVICE_PROPERTY_SITESE): The
  list of available sites on the device.
- [`QDMI_DEVICE_PROPERTY_OPERATIONS`](capi/constants.html.md#_CPPv4N22QDMI_DEVICE_PROPERTY_T31QDMI_DEVICE_PROPERTY_OPERATIONSE):
  The list of available calibrated operations on the device.
- [`QDMI_DEVICE_PROPERTY_COUPLINGMAP`](capi/constants.html.md#_CPPv4N22QDMI_DEVICE_PROPERTY_T32QDMI_DEVICE_PROPERTY_COUPLINGMAPE):
  The coupling map between qubits on the device.
- [`QDMI_DEVICE_PROPERTY_CUSTOM1`](capi/constants.html.md#_CPPv4N22QDMI_DEVICE_PROPERTY_T28QDMI_DEVICE_PROPERTY_CUSTOM1E): The
  current calibration set ID used by the session.

**Note:** Sites and qubits are not the same quantity. On Star-topology devices
the site list also contains the computational resonators, so it is longer than
the qubit count. Allocate registers from
[`QDMI_DEVICE_PROPERTY_QUBITSNUM`](capi/constants.html.md#_CPPv4N22QDMI_DEVICE_PROPERTY_T30QDMI_DEVICE_PROPERTY_QUBITSNUME) and
address hardware through the site list.

The following properties about every site (qubit) can be queried via the
[`IQM_QDMI_device_session_query_site_property()`](capi/device.html.md#_CPPv443IQM_QDMI_device_session_query_site_property23IQM_QDMI_Device_Session13IQM_QDMI_Site18QDMI_Site_Property6size_tPvP6size_t) function:

- [`QDMI_SITE_PROPERTY_NAME`](capi/constants.html.md#_CPPv4N20QDMI_SITE_PROPERTY_T23QDMI_SITE_PROPERTY_NAMEE): The name of
  the qubit (e.g., “QB1”, “QB2”).
- [`QDMI_SITE_PROPERTY_INDEX`](capi/constants.html.md#_CPPv4N20QDMI_SITE_PROPERTY_T24QDMI_SITE_PROPERTY_INDEXE): The index
  of the qubit.
- [`QDMI_SITE_PROPERTY_T1`](capi/constants.html.md#_CPPv4N20QDMI_SITE_PROPERTY_T21QDMI_SITE_PROPERTY_T1E): The T1
  coherence time of the qubit in microseconds (if available).
- [`QDMI_SITE_PROPERTY_T2`](capi/constants.html.md#_CPPv4N20QDMI_SITE_PROPERTY_T21QDMI_SITE_PROPERTY_T2E): The T2
  coherence time of the qubit in microseconds (if available).

The following properties about every operation can be queried via the
[`IQM_QDMI_device_session_query_operation_property()`](capi/device.html.md#_CPPv448IQM_QDMI_device_session_query_operation_property23IQM_QDMI_Device_Session18IQM_QDMI_Operation6size_tPK13IQM_QDMI_Site6size_tPKd23QDMI_Operation_Property6size_tPvP6size_t) function:

- [`QDMI_OPERATION_PROPERTY_NAME`](capi/constants.html.md#_CPPv4N25QDMI_OPERATION_PROPERTY_T28QDMI_OPERATION_PROPERTY_NAMEE):
  The name of the operation (e.g., “prx”, “cz”, “measure”).
- [`QDMI_OPERATION_PROPERTY_QUBITSNUM`](capi/constants.html.md#_CPPv4N25QDMI_OPERATION_PROPERTY_T33QDMI_OPERATION_PROPERTY_QUBITSNUME):
  The number of qubits the operation acts on.
- [`QDMI_OPERATION_PROPERTY_PARAMETERSNUM`](capi/constants.html.md#_CPPv4N25QDMI_OPERATION_PROPERTY_T37QDMI_OPERATION_PROPERTY_PARAMETERSNUME):
  The number of parameters the operation has.
- [`QDMI_OPERATION_PROPERTY_FIDELITY`](capi/constants.html.md#_CPPv4N25QDMI_OPERATION_PROPERTY_T32QDMI_OPERATION_PROPERTY_FIDELITYE):
  The fidelity of the operation (if available, may be qubit-specific).

**Note:** The available operations are determined by the current calibration
set. Quality metrics (T1, T2, fidelities) are fetched from the server’s
calibration set quality metrics endpoint if available. The QDMI device does not
support querying operation durations, as this information is not provided by the
IQM Server API.

## Submitting jobs

Set one or more programs in a common format with
[`IQM_QDMI_device_job_set_programs()`](capi/device.html.md#_CPPv432IQM_QDMI_device_job_set_programs19IQM_QDMI_Device_Job19QDMI_Program_Format6size_tPK6size_tPPCKv). Set the shared shot count with
[`QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM`](capi/constants.html.md#_CPPv4N27QDMI_DEVICE_JOB_PARAMETER_T34QDMI_DEVICE_JOB_PARAMETER_SHOTSNUME);
it must be positive and defaults to one. The device adds the session calibration
set ID.

Optional IQM `CircuitJobDefinition` fields go in a JSON object with exactly one
trailing NUL byte in
[`QDMI_DEVICE_JOB_PARAMETER_CUSTOM1`](capi/constants.html.md#_CPPv4N27QDMI_DEVICE_JOB_PARAMETER_T33QDMI_DEVICE_JOB_PARAMETER_CUSTOM1E):

```cpp
const std::string options = R"({"dd_mode":"enabled","active_reset_cycles":2})";
const auto status = IQM_QDMI_device_job_set_parameter(
    job, QDMI_DEVICE_JOB_PARAMETER_CUSTOM1, options.size() + 1,
    options.c_str());
```

Check `status` before submitting the job. The object applies to every program in
the job and replaces any previously set object. Omit it or set `{}` to use
server defaults. The device validates the JSON object and reserves `circuits`,
`shots`, and `calibration_set_id` for the programs, shot count, and session
configuration. The IQM service defines the supported optional fields and values
in its
[`PostJobsRequest` model](https://docs.iqm.tech/iqm-station-control-client/api/iqm.station_control.interface.models.circuit.PostJobsRequest.html).
Calibration jobs use a separate request format.

Execution requires the requested number of shots. Omit `heralding_mode` or use
`"none"`. Setting the shot-discarding `"zeros"` mode returns
`QDMI_ERROR_NOTSUPPORTED`; rejected options leave the job’s current settings
intact.

After submission,
[`QDMI_DEVICE_JOB_PROPERTY_QUEUEPOSITION`](capi/constants.html.md#_CPPv4N26QDMI_DEVICE_JOB_PROPERTY_T38QDMI_DEVICE_JOB_PROPERTY_QUEUEPOSITIONE)
reports the number of jobs ahead while the job is queued. Every property query
refreshes the job status and queue position from the IQM server. The query
returns `QDMI_ERROR_BADSTATE` when the refreshed job is not queued and
`QDMI_ERROR_NOTSUPPORTED` when the server does not provide a trustworthy queue
position.

The QDMI device currently supports the following program formats:

- **QIR Base Profile Strings**
  ([`QDMI_PROGRAM_FORMAT_QIRBASESTRING`](capi/constants.html.md#_CPPv4N21QDMI_PROGRAM_FORMAT_T33QDMI_PROGRAM_FORMAT_QIRBASESTRINGE)):
  QIR base profile programs as strings.
- **IQM JSON**
  ([`QDMI_PROGRAM_FORMAT_IQMJSON`](capi/constants.html.md#_CPPv4N21QDMI_PROGRAM_FORMAT_T27QDMI_PROGRAM_FORMAT_IQMJSONE)): IQM’s
  native JSON circuit format.

Pass QIR and JSON programs as strings with exactly one trailing NUL byte. The
program-list setter copies all programs before returning. They share the format,
shots per circuit, and other job parameters, and are submitted together in one
IQM job. Programs and results are indexed in input order, starting at zero; use
`IQM_QDMI_device_job_get_program` to read a stored program. IQM exposes one
outcome for the entire job; `IQM_QDMI_device_job_get_program_status` returns
`QDMI_ERROR_NOTSUPPORTED` for individual outcomes.

The number of programs is available through
`QDMI_DEVICE_JOB_PROPERTY_PROGRAMSNUM`.

## Retrieving jobs by ID

Use [`IQM_QDMI_device_session_retrieve_device_job_by_id()`](capi/device.html.md#_CPPv449IQM_QDMI_device_session_retrieve_device_job_by_id23IQM_QDMI_Device_SessionPKcP19IQM_QDMI_Device_Job) with the job
ID returned for an IQM circuit job by
[`QDMI_DEVICE_JOB_PROPERTY_ID`](capi/constants.html.md#_CPPv4N26QDMI_DEVICE_JOB_PROPERTY_T27QDMI_DEVICE_JOB_PROPERTY_IDE) to
obtain a new local handle for an existing IQM circuit job:

```cpp
IQM_QDMI_Device_Job retrieved_job = nullptr;
const int ret = IQM_QDMI_device_session_retrieve_device_job_by_id(
    session, job_id.c_str(), &retrieved_job);
```

The device validates the ID with the IQM Server using the current session
credentials and initializes the handle with the remote job’s current status.
Retrieving does not clone or submit the job. Parameters cannot be changed and
the retrieved handle cannot be submitted again. Freeing it only releases the
local handle; it does not cancel or delete the remote job. Check or wait for
completion before retrieving results. Retrieval reads the IQM job payload to
restore the program count and shots per circuit. JSON circuit payloads also
identify the IQM JSON format; other historical format metadata and original
program bytes are not reconstructed. Their property queries return
`QDMI_ERROR_NOTSUPPORTED`.

## Retrieving Job Results

After a job completes execution, you can retrieve the measurement results in
different formats. The IQM QDMI device supports retrieving results as histogram
counts or as individual shot measurements.

### Result Formats

The following result formats are supported:

- **[`QDMI_JOB_RESULT_HIST_KEYS`](capi/constants.html.md#_CPPv4N17QDMI_JOB_RESULT_T25QDMI_JOB_RESULT_HIST_KEYSE)**: Returns
  the bitstring keys from the measurement histogram as a comma-separated string.
- **[`QDMI_JOB_RESULT_HIST_VALUES`](capi/constants.html.md#_CPPv4N17QDMI_JOB_RESULT_T27QDMI_JOB_RESULT_HIST_VALUESE)**: Returns
  the corresponding count values as an array of `size_t`.
- **[`QDMI_JOB_RESULT_SHOTS`](capi/constants.html.md#_CPPv4N17QDMI_JOB_RESULT_T21QDMI_JOB_RESULT_SHOTSE)**: Returns
  individual shot measurements as a comma-separated string of bitstrings.

### Retrieving Histogram Results

Histogram results provide aggregated measurement counts for each unique outcome.
This is the most common format for analyzing quantum circuit results.

```cpp
// Wait for job completion
IQM_QDMI_device_job_wait(job, 0);

// Get histogram keys (bitstrings)
size_t keys_size = 0;
IQM_QDMI_device_job_get_results(job, 0, QDMI_JOB_RESULT_HIST_KEYS,
                                0, nullptr, &keys_size);
std::vector<char> keys_buffer(keys_size);
IQM_QDMI_device_job_get_results(job, 0, QDMI_JOB_RESULT_HIST_KEYS,
                                keys_size, keys_buffer.data(), nullptr);
std::string keys(keys_buffer.data());
// keys contains: "00,01,10,11" (example)

// Get histogram values (counts)
size_t values_size = 0;
IQM_QDMI_device_job_get_results(job, 0, QDMI_JOB_RESULT_HIST_VALUES,
                                0, nullptr, &values_size);
std::vector<size_t> values(values_size / sizeof(size_t));
IQM_QDMI_device_job_get_results(job, 0, QDMI_JOB_RESULT_HIST_VALUES,
                                values_size, values.data(), nullptr);
// values contains: {25, 15, 18, 6} (example counts for each key)
```

The histogram keys are returned as a comma-separated string, and the values are
returned as an array of counts. The keys and values are in the same order, so
after parsing the keys string by splitting on commas, the i-th parsed key
corresponds to `values[i]`.

Example of parsing the keys string:

```cpp
// Parse keys string into individual bitstrings
std::vector<std::string> key_list;
std::stringstream ss(keys);
std::string token;
while (std::getline(ss, token, ',')) {
  key_list.push_back(token);
}

// Now key_list[i] corresponds to values[i]
for (size_t i = 0; i < key_list.size(); ++i) {
  std::cout << "Outcome " << key_list[i] << ": " << values[i] << " times\n";
}
```

### Retrieving Individual Shot Measurements

Individual shot measurements provide the raw measurement outcome for each
execution of the circuit. This is useful for analyzing shot-to-shot correlations
or performing custom post-processing.

```cpp
// Wait for job completion
IQM_QDMI_device_job_wait(job, 0);

// Get individual shots
size_t shots_size = 0;
IQM_QDMI_device_job_get_results(job, 0, QDMI_JOB_RESULT_SHOTS,
                                0, nullptr, &shots_size);
std::vector<char> shots_buffer(shots_size);
IQM_QDMI_device_job_get_results(job, 0, QDMI_JOB_RESULT_SHOTS,
                                shots_size, shots_buffer.data(), nullptr);
std::string shots(shots_buffer.data());
// shots contains: "00,10,01,11,00,10,..." (one bitstring per shot)
```

Each bitstring in the result represents the measurement outcome for one shot.
The bitstrings are ordered chronologically (shot 1, shot 2, shot 3, etc.).

### Unsupported Result Formats

The following QDMI standard result formats (see [`QDMI_JOB_RESULT_T`](capi/constants.html.md#_CPPv417QDMI_JOB_RESULT_T))
are **not supported** by the IQM QDMI device because IQM quantum computers
return measurement data, not state vectors or probability distributions:

- [`QDMI_JOB_RESULT_STATEVECTOR_DENSE`](capi/constants.html.md#_CPPv4N17QDMI_JOB_RESULT_T33QDMI_JOB_RESULT_STATEVECTOR_DENSEE)
- [`QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS`](capi/constants.html.md#_CPPv4N17QDMI_JOB_RESULT_T39QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYSE)
  /
  [`QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES`](capi/constants.html.md#_CPPv4N17QDMI_JOB_RESULT_T41QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUESE)
- [`QDMI_JOB_RESULT_PROBABILITIES_DENSE`](capi/constants.html.md#_CPPv4N17QDMI_JOB_RESULT_T35QDMI_JOB_RESULT_PROBABILITIES_DENSEE)
- [`QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS`](capi/constants.html.md#_CPPv4N17QDMI_JOB_RESULT_T41QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYSE)
  /
  [`QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES`](capi/constants.html.md#_CPPv4N17QDMI_JOB_RESULT_T43QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUESE)

Attempting to retrieve these formats will return
[`QDMI_ERROR_NOTSUPPORTED`](capi/constants.html.md#_CPPv4N11QDMI_STATUS23QDMI_ERROR_NOTSUPPORTEDE).

## Triggering Calibration Jobs

Use [`IQM_QDMI_device_job_submit_calibration()`](capi/device.html.md#_CPPv438IQM_QDMI_device_job_submit_calibration19IQM_QDMI_Device_Job) from
`iqm_qdmi/calibration.h`. The extension returns `QDMI_ERROR_NOTSUPPORTED` if the
server does not support calibration jobs, as checked during session
initialization.

Create a regular IQM job and use `IQM_QDMI_device_job_set_programs` to set one
`QDMI_PROGRAM_FORMAT_IQMJSON` program containing the calibration configuration
as a NUL-terminated JSON string according to the IQM Server API. Call the
extension instead of `IQM_QDMI_device_job_submit`. The extension ignores the
program format, shot count, and circuit-specific parameters. After submission,
program-format and shot-count queries return `QDMI_ERROR_NOTSUPPORTED`. The
usual job check, wait, cancel, and free functions remain available.

The results can be retrieved via the
[`QDMI_JOB_RESULT_CUSTOM1`](capi/constants.html.md#_CPPv4N17QDMI_JOB_RESULT_T23QDMI_JOB_RESULT_CUSTOM1E) job result
parameter on a calibration job, which returns the new calibration set ID.

Querying the result of a calibration job returns its new calibration set ID. The
session continues to use its original calibration and cached device properties.
To use the new set, initialize a new session with its ID through
`QDMI_DEVICE_SESSION_PARAMETER_CUSTOM4`.

Here’s an example of submitting a calibration job:

```cpp
#include "iqm_qdmi/calibration.h"

int calibrate(IQM_QDMI_Device_Session session, const char *config,
              size_t config_size) {
  IQM_QDMI_Device_Job job = nullptr;
  auto status = IQM_QDMI_device_session_create_device_job(session, &job);
  if (status != QDMI_SUCCESS) {
    return status;
  }
  constexpr auto format = QDMI_PROGRAM_FORMAT_IQMJSON;
  const void *program = config;
  status = IQM_QDMI_device_job_set_programs(job, format, 1, &config_size,
                                        &program);
  if (status == QDMI_SUCCESS) {
    status = IQM_QDMI_device_job_submit_calibration(job);
  }
  if (status == QDMI_SUCCESS) {
    status = IQM_QDMI_device_job_wait(job, 0);
  }
  if (status == QDMI_SUCCESS) {
    /// Refresh the session's calibration data.
    size_t size = 0;
    status = IQM_QDMI_device_job_get_results(
        job, 0, QDMI_JOB_RESULT_CUSTOM1, 0, nullptr, &size);
  }
  IQM_QDMI_device_job_free(job);
  return status;
}
```

**Note:** Calibration jobs use different API endpoints than regular circuit
jobs:

- Submit: `/cocos/api/v4/calibration/runs` (calibration job endpoint)
- Status: `/cocos/api/v4/calibration/runs/<job_id>/status` (calibration job
  status endpoint)
- Abort: `/cocos/api/v4/calibration/runs/<job_id>/abort` (calibration job abort
  endpoint)

## Retrieving error logs

If a submitted job fails, the QDMI device will automatically log detailed error
information to help diagnose the problem. All errors are logged as ERROR level
messages, and any informational messages are logged as DEBUG level messages.

When you check a job’s status using [`IQM_QDMI_device_job_check()`](capi/device.html.md#_CPPv425IQM_QDMI_device_job_check19IQM_QDMI_Device_JobP15QDMI_Job_Status) and
the job has failed, all errors and messages will be automatically logged:

```cpp
auto *job = client.submit_job(TEST_PROGRAM, QDMI_PROGRAM_FORMAT_QIRBASESTRING);
IQM_QDMI_device_job_wait(job, 0);

QDMI_Job_Status status;
IQM_QDMI_device_job_check(job, &status);
// All errors and messages have already been logged automatically
// Check your log output for details about the failure
```

## Logging

The project provides a simple logging mechanism to help you debug your
application. You can control the logging level by setting the `IQM_LOG_LEVEL`
environment variable. The following logging levels are available:

- `NONE`: No logging.
- `ERROR`: Log only errors.
- `INFO`: Log errors and info messages.
- `DEBUG`: Log errors, info, and debug messages.

By default, the logging level is set to `ERROR`. Any other value disables
logging entirely. Messages at disabled levels are not constructed.

Logs are written to standard error. Set `IQM_LOG_LEVEL` before starting the
application; the logger reads it once, on first use.

`DEBUG` logs raw request and response bodies, including the bodies of failed
requests and malformed JSON responses, preserving their original formatting.
Treat that output as sensitive and avoid it in shared logs.

#### NOTE
`IQM_CPP_API_LOG_LEVEL` is a deprecated alias for `IQM_LOG_LEVEL`. It is only
read when `IQM_LOG_LEVEL` is unset or empty, and using it logs a notice at
`ERROR` level. It will be removed in a future release.

## Rate limiting

The IQM Server API meters requests against a per-account quota of 2000 units
over a rolling ten-second window, and blocks the account for 30 seconds once
that quota is exhausted. Submitting or cancelling a job costs 100 units and a
read costs 10, so twenty submissions inside one window run the quota out.

Every successful response reports `RateLimit-Limit` and `RateLimit-Remaining`. A
session follows what its own requests were told and waits out the rest of the
window once the remaining quota falls below ten percent of the limit, which is
far cheaper than the block it avoids. Set `IQM_RATE_LIMIT_THRESHOLD_PERCENT` to
another whole percentage to move that point, or to `0` to take the block
instead. The wait comes out of the timeout of the request that triggered it; a
request with less time than that left proceeds without waiting. Other clients
using the same token spend from the same quota, so the device still honors the
`Retry-After` header of an HTTP 429 response.
