# Copyright (c) 2025 - 2026 IQM Finland Oy
# All rights reserved.
#
# Licensed under the Apache License v2.0 with LLVM Exceptions (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://github.com/iqm-finland/QDMI-on-IQM/blob/main/LICENSE
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Serialization of Qiskit circuits into the IQM JSON program format.
MQT Core loads :func:`qiskit_to_iqm_json` through the
``mqt.core.qiskit.program_serializers`` entry point, so any QDMI backend over an
IQM device submits IQM JSON without naming this package.
"""
from __future__ import annotations
import json
import warnings
from typing import TYPE_CHECKING, Any
try:
from mqt.core.plugins.qiskit.exceptions import TranslationError, UnsupportedOperationError
from qiskit.circuit.library import Barrier, CZGate, Measure, RGate
except ImportError as e:
msg = (
"Failed to import Qiskit plugin. "
"Ensure that `iqm-qdmi` is installed with the `qiskit` extra, e.g., via `uv pip install iqm-qdmi[qiskit]`."
)
raise ImportError(msg) from e
from .gates import MoveGate
from .qiskit import IQMBackend
if TYPE_CHECKING:
from mqt.core.plugins.qiskit.backend import QDMIBackend
from qiskit.circuit import QuantumCircuit
__all__ = ["qiskit_to_iqm_json"]
def __dir__() -> list[str]:
return __all__
def _validate_metadata_keys(value: object) -> None:
"""Reject object keys that JSON encoding would silently convert to strings.
Args:
value: A metadata value to check recursively.
Raises:
TypeError: If an object key is not a string.
"""
if isinstance(value, dict):
for key, child in value.items():
if not isinstance(key, str):
msg = "Metadata object keys must be strings."
raise TypeError(msg)
_validate_metadata_keys(child)
elif isinstance(value, list | tuple):
for child in value:
_validate_metadata_keys(child)
def _json_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
"""Return metadata that encodes safely as JSON, or drop it with a warning.
Args:
metadata: Circuit metadata to check without mutating it.
Returns:
The metadata itself, or an empty dictionary if it cannot be encoded.
"""
try:
# Encode first to reject circular references before traversing the keys.
json.dumps(metadata, allow_nan=False)
_validate_metadata_keys(metadata)
except (TypeError, ValueError, RecursionError) as exc:
warnings.warn(f"Dropping circuit metadata that cannot be encoded as JSON: {exc}", stacklevel=3)
return {}
return metadata
[docs]
def qiskit_to_iqm_json(circuit: QuantumCircuit, backend: QDMIBackend) -> str:
"""Serialize a Qiskit :class:`~qiskit.circuit.QuantumCircuit` into IQM JSON.
The IQM JSON format is a device-specific format that encodes quantum operations
as JSON objects with site names, operation names, and arguments. Circuit
metadata is preserved using Python's JSON encoding (including tuples as
arrays), with string object keys and finite numbers required at every level.
Metadata that cannot be encoded is dropped with a warning; the circuit and
its metadata are not modified.
Note:
The serialization currently supports only operations that are natively
supported by the IQM hardware. Unsupported operations will raise
:class:`~mqt.core.plugins.qiskit.exceptions.UnsupportedOperationError`.
Args:
circuit: The Qiskit quantum circuit to serialize.
backend: The backend that runs the circuit. Its device provides the site
names the format uses as loci. For :class:`~iqm.qdmi.qiskit.IQMBackend`,
circuit indices follow the calibrated target's site order.
Returns:
JSON string representation of the circuit in IQM format.
Raises:
UnsupportedOperationError: If the circuit contains operations not supported
by IQM hardware.
TranslationError: If the serialization fails.
Examples:
>>> from qiskit import QuantumCircuit
>>> import numpy as np
>>> from iqm.qdmi.serializers import qiskit_to_iqm_json
>>> qc = QuantumCircuit(2, 2)
>>> qc.r(np.pi / 2, 0, 0)
>>> qc.cz(0, 1)
>>> qc.measure_all()
>>> json_str = qiskit_to_iqm_json(qc, backend)
"""
def _raise_error(exception_type: type[Exception], message: str) -> None:
"""Helper to raise exceptions (satisfies TRY301).
Args:
exception_type: The type of exception to raise.
message: The error message.
"""
raise exception_type(message)
try: # ruff:ignore[too-many-statements-in-try-clause]
# Check for unbound parameters
if circuit.parameters:
param_names = ", ".join(sorted(p.name for p in circuit.parameters))
msg = (
f"Circuit contains unbound parameters: {param_names}. "
"All parameters must be bound to numeric values before serialization to IQM JSON. "
"Use circuit.assign_parameters() to bind parameters."
)
_raise_error(UnsupportedOperationError, msg)
sites = backend.device.sites()
if isinstance(backend, IQMBackend):
sites = [sites[index] for index in backend.physical_qubits]
instructions: list[dict[str, Any]] = []
for instruction in circuit.data:
operation, qargs, cargs = instruction.operation, instruction.qubits, instruction.clbits
# R gate (PRX in IQM terminology)
if isinstance(operation, RGate):
qubit_index = circuit.find_bit(qargs[0]).index
instructions.append({
"name": "prx",
"locus": [sites[qubit_index].name()],
"args": {
"angle": float(operation.params[0]),
"phase": float(operation.params[1]),
},
})
# CZ or Move gate
elif isinstance(operation, CZGate | MoveGate):
qubit_index1: int = circuit.find_bit(qargs[0]).index
qubit_index2: int = circuit.find_bit(qargs[1]).index
instructions.append({
"name": operation.name,
"locus": [
sites[qubit_index1].name(),
sites[qubit_index2].name(),
],
"args": {},
})
# Barrier
elif isinstance(operation, Barrier):
qubit_indices: list[int] = []
for qubit in qargs:
qubit_index = circuit.find_bit(qubit).index
qubit_indices.append(qubit_index)
instructions.append({
"name": "barrier",
"locus": [sites[i].name() for i in qubit_indices],
"args": {},
})
# Measure
elif isinstance(operation, Measure):
clbit = cargs[0]
bitloc = circuit.find_bit(clbit)
# Check if classical bit is part of a register
if not bitloc.registers:
msg = (
"Measurement of unregistered classical bit is unsupported by IQM JSON export. "
"All classical bits must be part of a ClassicalRegister."
)
_raise_error(TranslationError, msg)
creg = bitloc.registers[0][0]
creg_idx = circuit.cregs.index(creg)
clbit_index = bitloc.registers[0][1]
key = f"{creg.name}_{len(creg)}_{creg_idx}_{clbit_index}"
qubit_index = circuit.find_bit(qargs[0]).index
instructions.append({
"name": "measure",
"locus": [sites[qubit_index].name()],
"args": {
"key": key,
},
})
# Unsupported operation
else:
msg = f"Operation '{operation.name}' is not supported in IQM JSON format"
_raise_error(UnsupportedOperationError, msg)
program: dict[str, Any] = {
"name": circuit.name or "circuit",
"metadata": _json_metadata(circuit.metadata),
"instructions": instructions,
}
return json.dumps(program)
except UnsupportedOperationError:
raise
except Exception as exc:
msg = f"Failed to serialize the circuit to IQM JSON: {exc}"
raise TranslationError(msg) from exc