# 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
from typing import TYPE_CHECKING, Any
try:
import numpy as np
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
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__
[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.
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.
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()
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):
angle_t = float(operation.params[0] / (2 * np.pi))
phase_t = float(operation.params[1] / (2 * np.pi))
qubit_index = circuit.find_bit(qargs[0]).index
instructions.append({
"name": "prx",
"locus": [sites[qubit_index].name()],
"args": {
"angle_t": angle_t,
"phase_t": phase_t,
},
})
# 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": {},
"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