Source code for Qdislib.core.backend.circuit_converter
#!/usr/bin/env python3
#
# Copyright 2002-2025 Barcelona Supercomputing Center (www.bsc.es)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
#
"""Convert circuits between the supported source formats (Qiskit / Qibo).
Qiskit and Qibo interconvert cleanly through OpenQASM 2.0. CUDA-Q kernels have
no stable QASM round-trip, so Qdislib feeds CUDA-Q via its backend-neutral DAG
instead; :meth:`CircuitConverter.num_qubits` still understands a CUDA-Q kernel
and the ``(dag, num_qubits)`` payload the cutting code uses for it.
"""
import logging
from typing import Any
logger = logging.getLogger(__name__)
def _qiskit_circuit():
"""The Qiskit ``QuantumCircuit`` class, or ``None`` if Qiskit isn't installed.
Imported lazily so ``import Qdislib`` works with only one backend SDK present
(a Qibo-only / Qiskit-only / CUDA-Q-only install). Conversions that need the
missing SDK then raise a clear ``TypeError`` instead of failing at import.
"""
try:
from qiskit import QuantumCircuit
return QuantumCircuit
except ImportError:
return None
def _qibo_circuit():
"""The Qibo ``Circuit`` class, or ``None`` if Qibo isn't installed."""
try:
from qibo import Circuit
return Circuit
except ImportError:
return None
def _pennylane_types():
"""The PennyLane circuit types ``(QuantumScript, QNode)``, or ``None`` if absent."""
try:
from pennylane.tape import QuantumScript
from pennylane import QNode
return (QuantumScript, QNode)
except ImportError:
return None
[docs]
class CircuitConverter:
"""Converts circuits between the supported formats and reports their size."""
[docs]
@staticmethod
def to_qiskit(circuit: Any):
qk, qb = _qiskit_circuit(), _qibo_circuit()
if qk and isinstance(circuit, qk):
return circuit
if qk and qb and isinstance(circuit, qb):
from qiskit.qasm2 import loads
return loads(circuit.to_qasm())
raise TypeError(f"Cannot convert {type(circuit).__name__} to a Qiskit circuit")
[docs]
@staticmethod
def to_qibo(circuit: Any):
qk, qb = _qiskit_circuit(), _qibo_circuit()
if qb and isinstance(circuit, qb):
return circuit
if qb and qk and isinstance(circuit, qk):
from qiskit.qasm2 import dumps
return qb.from_qasm(dumps(circuit))
raise TypeError(f"Cannot convert {type(circuit).__name__} to a Qibo circuit")
[docs]
@staticmethod
def to_qiskit_pauli(observable: str) -> str:
"""Reorder a qubit-0-first Pauli string to Qiskit's little-endian labels.
Qdislib observable strings are qubit-0-first (char ``i`` acts on qubit
``i``), matching the Qibo/CUDA-Q backends; a Qiskit ``Pauli`` /
``SparsePauliOp`` label is little-endian (char 0 = highest qubit).
Reversing keeps the convention consistent across backends -- centralised
here so the Qiskit-family backends (Aer, IQM) cannot drift apart.
"""
return observable[::-1]
[docs]
@staticmethod
def num_qubits(circuit: Any) -> int:
"""Qubit count for any supported circuit, including CUDA-Q payloads."""
qk, qb = _qiskit_circuit(), _qibo_circuit()
if qk and isinstance(circuit, qk):
return circuit.num_qubits
if qb and isinstance(circuit, qb):
return circuit.nqubits
# A PennyLane tape / QNode (the PennylaneCircuit wrapper is caught by the
# ``num_qubits`` attribute fall-back below).
pl = _pennylane_types()
if pl and isinstance(circuit, pl):
from Qdislib.utils.graph_pennylane import _to_tape
wires = [int(w) for w in _to_tape(circuit).wires]
return max(wires) + 1 if wires else 0
# The CUDA-Q ``(dag, num_qubits)`` payload (second element is the count).
if (
isinstance(circuit, (list, tuple))
and len(circuit) == 2
and isinstance(circuit[1], int)
):
return circuit[1]
# A ``[circuit, observable]`` wrapper (as returned by the dag_to_circuit_* helpers).
if (
isinstance(circuit, (list, tuple))
and circuit
and not isinstance(circuit[0], int)
):
return CircuitConverter.num_qubits(circuit[0])
# Fall back to a common attribute (covers CUDA-Q kernels exposing one).
for attr in ("num_qubits", "nqubits"):
value = getattr(circuit, attr, None)
if isinstance(value, int):
return value
# A built CUDA-Q kernel has no reliably-introspectable qubit count; the
# cutting code feeds CUDA-Q via the (dag, num_qubits) payload above, so
# this branch is only hit for a raw kernel. Report "size unknown" (0) so
# queue bookkeeping / can_run don't block it, rather than raising.
logger.debug(
"num_qubits: unknown circuit type %s; assuming size 0",
type(circuit).__name__,
)
return 0