Source code for pytket.extensions.qiskit.qiskit_convert

# Copyright 2019-2024 Quantinuum
#
# 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.


"""Methods to allow conversion between Qiskit and pytket circuit classes
"""
import warnings
from collections import defaultdict
from collections.abc import Iterable
from inspect import signature
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    Optional,
    TypeVar,
    cast,
)
from uuid import UUID

import numpy as np
import sympy
from numpy.typing import NDArray
from qiskit_ibm_runtime.models.backend_configuration import (  # type: ignore
    PulseBackendConfiguration,
)
from qiskit_ibm_runtime.models.backend_properties import (  # type: ignore
    BackendProperties,
)
from symengine import sympify  # type: ignore
from symengine.lib import symengine_wrapper  # type: ignore

import qiskit.circuit.library.standard_gates as qiskit_gates  # type: ignore
from pytket.architecture import Architecture, FullyConnected
from pytket.circuit import (
    Bit,
    CircBox,
    Circuit,
    Node,
    Op,
    OpType,
    QControlBox,
    Qubit,
    StatePreparationBox,
    Unitary1qBox,
    Unitary2qBox,
    Unitary3qBox,
    UnitType,
)
from pytket.passes import AutoRebase
from pytket.pauli import Pauli, QubitPauliString
from pytket.unit_id import _TEMP_BIT_NAME
from pytket.utils import (
    QubitPauliOperator,
    gen_term_sequence_circuit,
    permute_rows_cols_in_unitary,
)
from qiskit import (
    ClassicalRegister,
    QuantumCircuit,
    QuantumRegister,
)
from qiskit.circuit import (
    Barrier,
    Clbit,
    ControlledGate,
    Gate,
    Instruction,
    InstructionSet,
    Measure,
    Parameter,
    ParameterExpression,
    Reset,
)
from qiskit.circuit.library import (
    CRYGate,
    Initialize,
    PauliEvolutionGate,
    RYGate,
    StatePreparation,
    UnitaryGate,
)

if TYPE_CHECKING:
    from qiskit_ibm_runtime.ibm_backend import IBMBackend  # type: ignore
    from qiskit_ibm_runtime.models.backend_properties import Nduv

    from pytket.circuit import UnitID
    from qiskit.circuit.quantumcircuitdata import QuantumCircuitData  # type: ignore

_qiskit_gates_1q = {
    # Exact equivalents (same signature except for factor of pi in each parameter):
    qiskit_gates.HGate: OpType.H,
    qiskit_gates.IGate: OpType.noop,
    qiskit_gates.PhaseGate: OpType.U1,
    qiskit_gates.RGate: OpType.PhasedX,
    qiskit_gates.RXGate: OpType.Rx,
    qiskit_gates.RYGate: OpType.Ry,
    qiskit_gates.RZGate: OpType.Rz,
    qiskit_gates.SdgGate: OpType.Sdg,
    qiskit_gates.SGate: OpType.S,
    qiskit_gates.SXdgGate: OpType.SXdg,
    qiskit_gates.SXGate: OpType.SX,
    qiskit_gates.TdgGate: OpType.Tdg,
    qiskit_gates.TGate: OpType.T,
    qiskit_gates.U1Gate: OpType.U1,
    qiskit_gates.U2Gate: OpType.U2,
    qiskit_gates.U3Gate: OpType.U3,
    qiskit_gates.UGate: OpType.U3,
    qiskit_gates.XGate: OpType.X,
    qiskit_gates.YGate: OpType.Y,
    qiskit_gates.ZGate: OpType.Z,
}

_qiskit_gates_2q = {
    # Exact equivalents (same signature except for factor of pi in each parameter):
    qiskit_gates.CHGate: OpType.CH,
    qiskit_gates.CPhaseGate: OpType.CU1,
    qiskit_gates.CRXGate: OpType.CRx,
    qiskit_gates.CRYGate: OpType.CRy,
    qiskit_gates.CRZGate: OpType.CRz,
    qiskit_gates.CUGate: OpType.CU3,
    qiskit_gates.CU1Gate: OpType.CU1,
    qiskit_gates.CU3Gate: OpType.CU3,
    qiskit_gates.CXGate: OpType.CX,
    qiskit_gates.CSXGate: OpType.CSX,
    qiskit_gates.CYGate: OpType.CY,
    qiskit_gates.CZGate: OpType.CZ,
    qiskit_gates.ECRGate: OpType.ECR,
    qiskit_gates.iSwapGate: OpType.ISWAPMax,
    qiskit_gates.RXXGate: OpType.XXPhase,
    qiskit_gates.RYYGate: OpType.YYPhase,
    qiskit_gates.RZZGate: OpType.ZZPhase,
    qiskit_gates.SwapGate: OpType.SWAP,
}

_qiskit_gates_other = {
    # Exact equivalents (same signature except for factor of pi in each parameter):
    qiskit_gates.C3XGate: OpType.CnX,
    qiskit_gates.C4XGate: OpType.CnX,
    qiskit_gates.CCXGate: OpType.CCX,
    qiskit_gates.CCZGate: OpType.CnZ,
    qiskit_gates.CSwapGate: OpType.CSWAP,
    # Multi-controlled gates (qiskit expects a list of controls followed by the target):
    qiskit_gates.MCXGate: OpType.CnX,
    qiskit_gates.MCXGrayCode: OpType.CnX,
    qiskit_gates.MCXRecursive: OpType.CnX,
    qiskit_gates.MCXVChain: OpType.CnX,
    # Special types:
    Barrier: OpType.Barrier,
    Instruction: OpType.CircBox,
    Gate: OpType.CircBox,
    Measure: OpType.Measure,
    Reset: OpType.Reset,
    Initialize: OpType.StatePreparationBox,
    StatePreparation: OpType.StatePreparationBox,
}

_known_qiskit_gate = {**_qiskit_gates_1q, **_qiskit_gates_2q, **_qiskit_gates_other}

# Some qiskit gates are aliases (e.g. UGate and U3Gate).
# In such cases this reversal will select one or the other.
_known_qiskit_gate_rev = {v: k for k, v in _known_qiskit_gate.items()}

# Ensure U3 maps to UGate. (U3Gate deprecated in Qiskit but equivalent.)
_known_qiskit_gate_rev[OpType.U3] = qiskit_gates.UGate

# There is a bijective mapping, but requires some special parameter conversions
# tk1(a, b, c) = U(b, a-1/2, c+1/2) + phase(-(a+c)/2)
_known_qiskit_gate_rev[OpType.TK1] = qiskit_gates.UGate

# some gates are only equal up to global phase, support their conversion
# from tket -> qiskit
_known_gate_rev_phase = {
    optype: (qgate, 0.0) for optype, qgate in _known_qiskit_gate_rev.items()
}

_known_gate_rev_phase[OpType.V] = (qiskit_gates.SXGate, -0.25)
_known_gate_rev_phase[OpType.Vdg] = (qiskit_gates.SXdgGate, 0.25)

# use minor signature hacks to figure out the string names of qiskit Gate objects
_gate_str_2_optype: dict[str, OpType] = dict()
for gate, optype in _known_qiskit_gate.items():
    if gate in (
        UnitaryGate,
        Instruction,
        Gate,
        qiskit_gates.MCXGate,  # all of these have special (c*n)x names
        qiskit_gates.MCXGrayCode,
        qiskit_gates.MCXRecursive,
        qiskit_gates.MCXVChain,
    ):
        continue
    sig = signature(gate.__init__)
    # name is only a property of the instance, not the class
    # so initialize with the correct number of dummy variables
    n_params = len([p for p in sig.parameters.values() if p.default is p.empty]) - 1
    name = gate(*([1] * n_params)).name
    _gate_str_2_optype[name] = optype

_gate_str_2_optype_rev = {v: k for k, v in _gate_str_2_optype.items()}
# the aliasing of the name is ok in the reverse map
_gate_str_2_optype_rev[OpType.Unitary1qBox] = "unitary"


def _tk_gate_set(config: PulseBackendConfiguration) -> set[OpType]:
    """Set of tket gate types supported by the qiskit backend"""
    if config.simulator:
        gate_set = {
            _gate_str_2_optype[gate_str]
            for gate_str in config.basis_gates
            if gate_str in _gate_str_2_optype
        }.union({OpType.Measure, OpType.Reset, OpType.Barrier})
        return gate_set

    else:
        return {
            _gate_str_2_optype[gate_str]
            for gate_str in config.supported_instructions
            if gate_str in _gate_str_2_optype
        }


def _qpo_from_peg(peg: PauliEvolutionGate, qubits: list[Qubit]) -> QubitPauliOperator:
    op = peg.operator
    t = peg.params[0]
    qpodict = {}
    for p, c in zip(op.paulis, op.coeffs):
        if np.iscomplex(c):
            raise ValueError(f"Coefficient for Pauli {p} is non-real.")
        coeff = param_to_tk(t) * c
        qpslist = []
        pstr = p.to_label()
        for a in pstr:
            if a == "X":
                qpslist.append(Pauli.X)
            elif a == "Y":
                qpslist.append(Pauli.Y)
            elif a == "Z":
                qpslist.append(Pauli.Z)
            else:
                assert a == "I"
                qpslist.append(Pauli.I)
        qpodict[QubitPauliString(qubits, qpslist)] = coeff
    return QubitPauliOperator(qpodict)


def _string_to_circuit(
    circuit_string: str,
    n_qubits: int,
    qiskit_prep: Initialize | StatePreparation,
) -> Circuit:
    """Helper function to generate circuits for Initialize
    and StatePreparation objects built with strings"""

    circ = Circuit(n_qubits)
    # Check if Instruction is Initialize or Statepreparation
    # If Initialize, add resets
    if isinstance(qiskit_prep, Initialize):
        for qubit in circ.qubits:
            circ.Reset(qubit)

    # We iterate through the string in reverse to add the
    # gates in the correct order (endian-ness).
    for qubit_index, character in enumerate(reversed(circuit_string)):
        match character:
            case "0":
                pass
            case "1":
                circ.X(qubit_index)
            case "+":
                circ.H(qubit_index)
            case "-":
                circ.X(qubit_index)
                circ.H(qubit_index)
            case "r":
                circ.H(qubit_index)
                circ.S(qubit_index)
            case "l":
                circ.H(qubit_index)
                circ.Sdg(qubit_index)
            case _:
                raise ValueError(
                    f"Cannot parse string for character {character}. "
                    + "The supported characters are {'0', '1', '+', '-', 'r', 'l'}."
                )

    return circ


def _get_pytket_ctrl_state(bitstring: str, n_bits: int) -> tuple[bool, ...]:
    "Converts a little endian string '001'=1 (LE) to (1, 0, 0)."
    assert set(bitstring).issubset({"0", "1"})
    padded_bitstring = bitstring.zfill(n_bits)
    pytket_ctrl_state = reversed([bool(int(b)) for b in padded_bitstring])
    return tuple(pytket_ctrl_state)


def _all_bits_set(integer: int, n_bits: int) -> bool:
    return integer.bit_count() == n_bits


def _get_controlled_tket_optype(c_gate: ControlledGate) -> OpType:
    """Get a pytket contolled OpType from a qiskit ControlledGate."""

    # If the control state is not "all |1>", use QControlBox
    if not _all_bits_set(c_gate.ctrl_state, c_gate.num_ctrl_qubits):
        return OpType.QControlBox

    elif c_gate.base_class in _known_qiskit_gate:
        # First we check if the gate is in _known_qiskit_gate
        # this avoids CZ being converted to CnZ
        return _known_qiskit_gate[c_gate.base_class]

    match c_gate.base_gate.base_class:
        case qiskit_gates.RYGate:
            return OpType.CnRy
        case qiskit_gates.YGate:
            return OpType.CnY
        case qiskit_gates.ZGate:
            return OpType.CnZ
        case _:
            if (
                c_gate.base_gate.base_class in _known_qiskit_gate
                or c_gate.base_gate.base_class is UnitaryGate
            ):
                return OpType.QControlBox
            else:
                raise NotImplementedError(
                    "Conversion of qiskit ControlledGate with base gate "
                    + f"base gate {c_gate.base_gate}"
                    + "not implemented."
                )


def _optype_from_qiskit_instruction(instruction: Instruction) -> OpType:
    """Get a pytket OpType from a qiskit Instruction."""
    if isinstance(instruction, ControlledGate):
        return _get_controlled_tket_optype(instruction)
    try:
        optype = _known_qiskit_gate[instruction.base_class]
        return optype
    except KeyError:
        raise NotImplementedError(
            f"Conversion of qiskit's {instruction.name} instruction is "
            + "currently unsupported by qiskit_to_tk. Consider "
            + "using QuantumCircuit.decompose() before attempting "
            + "conversion."
        )


UnitaryBox = Unitary1qBox | Unitary2qBox | Unitary3qBox


def _get_unitary_box(unitary: NDArray[np.complex128], num_qubits: int) -> UnitaryBox:
    match num_qubits:
        case 1:
            assert unitary.shape == (2, 2)
            return Unitary1qBox(unitary)
        case 2:
            assert unitary.shape == (4, 4)
            return Unitary2qBox(unitary)
        case 3:
            assert unitary.shape == (8, 8)
            return Unitary3qBox(unitary)
        case _:
            raise NotImplementedError(
                f"Conversion of {num_qubits}-qubit unitary gates not supported."
            )


def _get_qcontrol_box(c_gate: ControlledGate, params: list[float]) -> QControlBox:
    qiskit_ctrl_state: str = bin(c_gate.ctrl_state)[2:]
    pytket_ctrl_state: tuple[bool, ...] = _get_pytket_ctrl_state(
        bitstring=qiskit_ctrl_state, n_bits=c_gate.num_ctrl_qubits
    )
    if isinstance(c_gate.base_gate, UnitaryGate):
        unitary = c_gate.base_gate.params[0]
        # Here we reverse the order of the columns to correct for endianness.
        new_unitary: NDArray[np.complex128] = permute_rows_cols_in_unitary(
            matrix=unitary,
            permutation=tuple(reversed(range(c_gate.base_gate.num_qubits))),
        )
        base_op: Op = _get_unitary_box(new_unitary, c_gate.base_gate.num_qubits)
    else:
        base_tket_gate: OpType = _known_qiskit_gate[c_gate.base_gate.base_class]

        base_op: Op = Op.create(base_tket_gate, params)  # type: ignore

    return QControlBox(
        base_op, n_controls=c_gate.num_ctrl_qubits, control_state=pytket_ctrl_state
    )


def _add_state_preparation(
    tkc: Circuit, qubits: list[Qubit], prep: Initialize | StatePreparation
) -> None:
    """Handles different cases of Initialize and StatePreparation
    and appends the appropriate state preparation to a Circuit instance."""

    # Check how Initialize or StatePrep is constructed
    # With a string, an int or an array of amplitudes
    if len(prep.params) != 1:
        if isinstance(prep.params[0], str):
            # Parse string to get the right single qubit gates
            circuit_string: str = "".join(prep.params)
            circuit = _string_to_circuit(
                circuit_string, prep.num_qubits, qiskit_prep=prep
            )
            tkc.add_circuit(circuit, qubits)
        else:
            amplitude_array: NDArray[np.complex128] = np.array(prep.params)
            pytket_state_prep_box = StatePreparationBox(
                amplitude_array, with_initial_reset=(type(prep) is Initialize)
            )

            # Need to reverse qubits here (endian-ness)
            reversed_qubits = list(reversed(qubits))
            tkc.add_gate(pytket_state_prep_box, reversed_qubits)
    elif isinstance(prep.params[0], complex):
        # convert int to a binary string and apply X for |1>
        integer_parameter = int(prep.params[0].real)
        bit_string = bin(integer_parameter)[2:]
        circuit = _string_to_circuit(bit_string, prep.num_qubits, qiskit_prep=prep)
        tkc.add_circuit(circuit, qubits)
    else:
        raise TypeError(
            "Unrecognised type of Instruction.params "
            + "when trying to convert Initialize or StatePreparation instruction."
        )


def _get_pytket_condition_kwargs(
    instruction: Instruction,
    cregmap: dict[str, ClassicalRegister],
    circuit: QuantumCircuit,
) -> dict[str, Any]:
    if type(instruction.condition[0]) is ClassicalRegister:
        cond_reg = cregmap[instruction.condition[0]]
        condition_kwargs = {
            "condition_bits": [cond_reg[k] for k in range(len(cond_reg))],
            "condition_value": instruction.condition[1],
        }
        return condition_kwargs
    elif type(instruction.condition[0]) is Clbit:
        # .find_bit() returns type:
        #    tuple[index, list[tuple[ClassicalRegister, index]]]
        # We assume each bit belongs to exactly one register.
        index = circuit.find_bit(instruction.condition[0])[0]
        register = circuit.find_bit(instruction.condition[0])[1][0][0]
        cond_reg = cregmap[register]
        condition_kwargs = {
            "condition_bits": [cond_reg[index]],
            "condition_value": instruction.condition[1],
        }
        return condition_kwargs
    else:
        raise NotImplementedError("condition must contain classical bit or register")


def _build_circbox(instr: Instruction, circuit: QuantumCircuit) -> CircBox:
    qregs = [QuantumRegister(instr.num_qubits, "q")] if instr.num_qubits > 0 else []
    cregs = [ClassicalRegister(instr.num_clbits, "c")] if instr.num_clbits > 0 else []
    builder = CircuitBuilder(qregs, cregs)
    builder.add_qiskit_data(circuit, instr.definition)
    subc = builder.circuit()
    subc.name = instr.name
    return CircBox(subc)


class CircuitBuilder:
    def __init__(
        self,
        qregs: list[QuantumRegister],
        cregs: Optional[list[ClassicalRegister]] = None,
        name: Optional[str] = None,
        phase: Optional[sympy.Expr] = None,
    ):
        self.qregs = qregs
        self.cregs = [] if cregs is None else cregs
        self.qbmap = {}
        self.cbmap = {}
        if name is not None:
            self.tkc = Circuit(name=name)
        else:
            self.tkc = Circuit()
        if phase is not None:
            self.tkc.add_phase(phase)
        for reg in qregs:
            self.tkc.add_q_register(reg.name, len(reg))
            for i, qb in enumerate(reg):
                self.qbmap[qb] = Qubit(reg.name, i)
        self.cregmap = {}
        for reg in self.cregs:
            tk_reg = self.tkc.add_c_register(reg.name, len(reg))
            self.cregmap.update({reg: tk_reg})
            for i, cb in enumerate(reg):
                self.cbmap[cb] = Bit(reg.name, i)

    def circuit(self) -> Circuit:
        return self.tkc

    def add_qiskit_data(
        self, circuit: QuantumCircuit, data: Optional["QuantumCircuitData"] = None
    ) -> None:
        data = data or circuit.data
        for datum in data:
            instr, qargs, cargs = datum.operation, datum.qubits, datum.clbits

            qubits: list[Qubit] = [self.qbmap[qbit] for qbit in qargs]
            bits: list[Bit] = [self.cbmap[bit] for bit in cargs]

            condition_kwargs = {}
            if instr.condition is not None:
                condition_kwargs = _get_pytket_condition_kwargs(
                    instruction=instr,
                    cregmap=self.cregmap,
                    circuit=circuit,
                )

            optype = None
            if type(instr) not in (PauliEvolutionGate, UnitaryGate):
                # Handling of PauliEvolutionGate and UnitaryGate below
                optype = _optype_from_qiskit_instruction(instruction=instr)

            if optype == OpType.QControlBox:
                params = [param_to_tk(p) for p in instr.base_gate.params]
                q_ctrl_box = _get_qcontrol_box(c_gate=instr, params=params)
                self.tkc.add_qcontrolbox(q_ctrl_box, qubits)

            elif optype == OpType.StatePreparationBox:
                # Append OpType found by stateprep helpers
                _add_state_preparation(self.tkc, qubits, instr)

            elif type(instr) is PauliEvolutionGate:
                qpo = _qpo_from_peg(instr, qubits)
                empty_circ = Circuit(len(qargs))
                circ = gen_term_sequence_circuit(qpo, empty_circ)
                ccbox = CircBox(circ)
                self.tkc.add_circbox(ccbox, qubits)

            elif type(instr) is UnitaryGate:
                unitary = cast(NDArray[np.complex128], instr.params[0])
                if len(qubits) == 0:
                    # If the UnitaryGate acts on no qubits, we add a phase.
                    self.tkc.add_phase(np.angle(unitary[0][0]) / np.pi)
                else:
                    unitary_box = _get_unitary_box(
                        unitary=unitary, num_qubits=instr.num_qubits
                    )
                    self.tkc.add_gate(
                        unitary_box,
                        list(reversed(qubits)),
                        **condition_kwargs,
                    )

            elif optype == OpType.Barrier:
                self.tkc.add_barrier(qubits)

            elif optype == OpType.CircBox:
                circbox = _build_circbox(instr, circuit)
                self.tkc.add_circbox(circbox, qubits + bits, **condition_kwargs)  # type: ignore

            elif optype == OpType.CU3 and type(instr) is qiskit_gates.CUGate:
                if instr.params[-1] == 0:
                    self.tkc.add_gate(
                        optype,
                        [param_to_tk(p) for p in instr.params[:-1]],
                        qubits,
                        **condition_kwargs,
                    )
                else:
                    raise NotImplementedError("CUGate with nonzero phase")
            else:
                params = [param_to_tk(p) for p in instr.params]
                self.tkc.add_gate(optype, params, qubits + bits, **condition_kwargs)  # type: ignore


[docs] def qiskit_to_tk(qcirc: QuantumCircuit, preserve_param_uuid: bool = False) -> Circuit: """ Converts a qiskit :py:class:`qiskit.QuantumCircuit` to a pytket :py:class:`Circuit`. :param qcirc: A circuit to be converted :param preserve_param_uuid: Whether to preserve symbolic Parameter uuids by appending them to the tket Circuit symbol names as "_UUID:<uuid>". This can be useful if you want to reassign Parameters after conversion to tket and back, as it is necessary for Parameter object equality to be preserved. :return: The converted circuit """ circ_name = qcirc.name # Parameter uses a hidden _uuid for equality check # we optionally preserve this in parameter name for later use if preserve_param_uuid: updates = {p: Parameter(f"{p.name}_UUID:{p._uuid}") for p in qcirc.parameters} qcirc = cast(QuantumCircuit, qcirc.assign_parameters(updates)) builder = CircuitBuilder( qregs=qcirc.qregs, cregs=qcirc.cregs, name=circ_name, phase=param_to_tk(qcirc.global_phase), ) builder.add_qiskit_data(qcirc) return builder.circuit()
def _get_qiskit_control_state(bool_list: list[bool]) -> str: return "".join(str(int(b)) for b in bool_list)[::-1] def param_to_tk(p: float | ParameterExpression) -> sympy.Expr: if isinstance(p, ParameterExpression): symexpr = p._symbol_expr try: return symexpr._sympy_() / sympy.pi except AttributeError: return symexpr / sympy.pi else: return p / sympy.pi def param_to_qiskit( p: sympy.Expr, symb_map: dict[Parameter, sympy.Symbol] ) -> float | ParameterExpression: ppi = p * sympy.pi if len(ppi.free_symbols) == 0: return float(ppi.evalf()) else: return ParameterExpression(symb_map, sympify(ppi)) def _get_params( op: Op, symb_map: dict[Parameter, sympy.Symbol] ) -> list[float | ParameterExpression]: return [param_to_qiskit(p, symb_map) for p in op.params] def append_tk_command_to_qiskit( op: "Op", args: list["UnitID"], qcirc: QuantumCircuit, qregmap: dict[str, QuantumRegister], cregmap: dict[str, ClassicalRegister], symb_map: dict[Parameter, sympy.Symbol], range_preds: dict[Bit, tuple[list["UnitID"], int]], ) -> InstructionSet: optype = op.type if optype == OpType.Measure: qubit = args[0] bit = args[1] qb = qregmap[qubit.reg_name][qubit.index[0]] b = cregmap[bit.reg_name][bit.index[0]] return qcirc.measure(qb, b) if optype == OpType.Reset: qb = qregmap[args[0].reg_name][args[0].index[0]] return qcirc.reset(qb) if optype in [OpType.CircBox, OpType.ExpBox, OpType.PauliExpBox, OpType.CustomGate]: subcircuit = op.get_circuit() # type: ignore subqc = tk_to_qiskit(subcircuit) qargs = [] cargs = [] for a in args: if a.type == UnitType.qubit: qargs.append(qregmap[a.reg_name][a.index[0]]) else: cargs.append(cregmap[a.reg_name][a.index[0]]) if optype == OpType.CustomGate: instruc = subqc.to_gate() instruc.name = op.get_name() else: instruc = subqc.to_instruction() return qcirc.append(instruc, qargs, cargs) if optype in [OpType.Unitary1qBox, OpType.Unitary2qBox, OpType.Unitary3qBox]: qargs = [qregmap[q.reg_name][q.index[0]] for q in args] u = op.get_matrix() # type: ignore g = UnitaryGate(u, label="unitary") # Note reversal of qubits, to account for endianness (pytket unitaries are # ILO-BE == DLO-LE; qiskit unitaries are ILO-LE == DLO-BE). return qcirc.append(g, qargs=list(reversed(qargs))) if optype == OpType.StatePreparationBox: qargs = [qregmap[q.reg_name][q.index[0]] for q in args] statevector_array = op.get_statevector() # type: ignore # check if the StatePreparationBox contains resets if op.with_initial_reset(): # type: ignore initializer = Initialize(statevector_array) return qcirc.append(initializer, qargs=list(reversed(qargs))) else: qiskit_state_prep_box = StatePreparation(statevector_array) return qcirc.append(qiskit_state_prep_box, qargs=list(reversed(qargs))) if optype == OpType.QControlBox: assert isinstance(op, QControlBox) qargs = [qregmap[q.reg_name][q.index[0]] for q in args] pytket_control_state: list[bool] = op.get_control_state_bits() qiskit_control_state: str = _get_qiskit_control_state(pytket_control_state) try: gatetype, phase = _known_gate_rev_phase[op.get_op().type] except KeyError: raise NotImplementedError( "Conversion of QControlBox with base gate" + f"{op.get_op()} not supported by tk_to_qiskit." ) params = _get_params(op.get_op(), symb_map) operation = gatetype(*params) return qcirc.append( operation.control( num_ctrl_qubits=op.get_n_controls(), ctrl_state=qiskit_control_state ), qargs=qargs, ) if optype == OpType.Barrier: if any(q.type == UnitType.bit for q in args): raise NotImplementedError( "Qiskit Barriers are not defined for classical bits." ) qargs = [qregmap[q.reg_name][q.index[0]] for q in args] g = Barrier(len(args)) return qcirc.append(g, qargs=qargs) if optype == OpType.RangePredicate: if op.lower != op.upper: # type: ignore raise NotImplementedError range_preds[args[-1]] = (args[:-1], op.lower) # type: ignore # attach predicate to bit, # subsequent conditional will handle it return Instruction("", 0, 0, []) if optype == OpType.Conditional: if op.op.type == OpType.Phase: # type: ignore # conditional phase not supported return InstructionSet() if args[0] in range_preds: assert op.value == 1 # type: ignore condition_bits, value = range_preds[args[0]] # type: ignore del range_preds[args[0]] # type: ignore args = condition_bits + args[1:] width = len(condition_bits) else: width = op.width # type: ignore value = op.value # type: ignore regname = args[0].reg_name for i, a in enumerate(args[:width]): if a.reg_name != regname: raise NotImplementedError("Conditions can only use a single register") instruction = append_tk_command_to_qiskit( op.op, # type: ignore args[width:], qcirc, qregmap, cregmap, symb_map, range_preds, ) if len(cregmap[regname]) == width: for i, a in enumerate(args[:width]): if a.index != [i]: raise NotImplementedError( """Conditions must be an entire register in\ order or only one bit of one register""" ) instruction.c_if(cregmap[regname], value) elif width == 1: instruction.c_if(cregmap[regname][args[0].index[0]], value) else: raise NotImplementedError( """Conditions must be an entire register in\ order or only one bit of one register""" ) return instruction # normal gates qargs = [qregmap[q.reg_name][q.index[0]] for q in args] if optype == OpType.CnX: return qcirc.mcx(qargs[:-1], qargs[-1]) if optype == OpType.CnY: return qcirc.append(qiskit_gates.YGate().control(len(qargs) - 1), qargs) if optype == OpType.CnZ: new_gate = qiskit_gates.ZGate().control(len(qargs) - 1) new_gate.name = "mcz" return qcirc.append(new_gate, qargs) if optype == OpType.CnRy: # might as well do a bit more checking assert len(op.params) == 1 alpha = param_to_qiskit(op.params[0], symb_map) assert len(qargs) >= 2 if len(qargs) == 2: # presumably more efficient; single control only new_gate = CRYGate(alpha) else: new_gate = RYGate(alpha).control(len(qargs) - 1) qcirc.append(new_gate, qargs) return qcirc if optype == OpType.CU3: params = _get_params(op, symb_map) + [0] return qcirc.append(qiskit_gates.CUGate(*params), qargs=qargs) if optype == OpType.TK1: params = _get_params(op, symb_map) half = ParameterExpression(symb_map, sympify(sympy.pi / 2)) qcirc.global_phase += -params[0] / 2 - params[2] / 2 return qcirc.append( qiskit_gates.UGate(params[1], params[0] - half, params[2] + half), qargs=qargs, ) if optype == OpType.Phase: params = _get_params(op, symb_map) assert len(params) == 1 qcirc.global_phase += params[0] return InstructionSet() # others are direct translations try: gatetype, phase = _known_gate_rev_phase[optype] except KeyError as error: raise NotImplementedError( "Cannot convert tket Op to Qiskit gate: " + op.get_name() ) from error params = _get_params(op, symb_map) g = gatetype(*params) if type(phase) is float: qcirc.global_phase += phase * np.pi else: qcirc.global_phase += sympify(phase * sympy.pi) return qcirc.append(g, qargs=qargs) # The set of tket gates that can be converted directly to qiskit gates _supported_tket_gates = set(_known_gate_rev_phase.keys()) _additional_multi_controlled_gates = {OpType.CnY, OpType.CnZ, OpType.CnRy} # tket gates which are protected from being decomposed in the rebase _protected_tket_gates = ( _supported_tket_gates | _additional_multi_controlled_gates | { OpType.Unitary1qBox, OpType.Unitary2qBox, OpType.Unitary3qBox, OpType.QControlBox, } | {OpType.CustomGate} ) # This is a rebase to the set of tket gates which have an exact substitution in qiskit supported_gate_rebase = AutoRebase(_protected_tket_gates) def _has_implicit_permutation(circ: Circuit) -> bool: """Returns True if a Circuit has a non-trivial permutation of qubits, false otherwise.""" return any(q0 != q1 for q0, q1 in circ.implicit_qubit_permutation().items())
[docs] def tk_to_qiskit( tkcirc: Circuit, replace_implicit_swaps: bool = False, perm_warning: bool = True, ) -> QuantumCircuit: """ Converts a pytket :py:class:`Circuit` to a qiskit :py:class:`qiskit.QuantumCircuit`. In many cases there will be a qiskit gate to exactly replace each tket gate. If no exact replacement can be found for a part of the circuit then an equivalent circuit will be returned using the tket gates which are supported in qiskit. Note that implicit swaps in a pytket Circuit are not handled by default. Consider using the replace_implicit_swaps flag to replace these implicit swaps with SWAP gates. :param tkcirc: A :py:class:`Circuit` to be converted :param replace_implicit_swaps: Implement implicit permutation by adding SWAPs to the end of the circuit. :param perm_warning: Warn if an input circuit has implicit qubit permutations, and `replace_implicit_swaps` is `False`. True by default. :return: The converted circuit """ tkc = tkcirc.copy() # Make a local copy of tkcirc if replace_implicit_swaps: tkc.replace_implicit_wire_swaps() if ( _has_implicit_permutation(tkcirc) and perm_warning and not replace_implicit_swaps ): warnings.warn( "The pytket Circuit contains implicit qubit permutations" + " which aren't handled by default." + " Consider using the replace_implicit_swaps flag in tk_to_qiskit or" + " replacing them using Circuit.replace_implicit_swaps()." ) qcirc = QuantumCircuit(name=tkc.name) qreg_sizes: dict[str, int] = {} for qb in tkc.qubits: if len(qb.index) != 1: raise NotImplementedError("Qiskit registers must use a single index") if (qb.reg_name not in qreg_sizes) or (qb.index[0] >= qreg_sizes[qb.reg_name]): qreg_sizes.update({qb.reg_name: qb.index[0] + 1}) c_regs = tkcirc.c_registers if set(bit for reg in c_regs for bit in reg) != set(tkcirc.bits): raise NotImplementedError("Bit registers must be singly indexed from zero") qregmap = {} for reg_name, size in qreg_sizes.items(): qis_reg = QuantumRegister(size, reg_name) qregmap.update({reg_name: qis_reg}) qcirc.add_register(qis_reg) cregmap = {} for c_reg in c_regs: if c_reg.name != _TEMP_BIT_NAME: qis_reg = ClassicalRegister(c_reg.size, c_reg.name) cregmap.update({c_reg.name: qis_reg}) qcirc.add_register(qis_reg) symb_map = {Parameter(str(s)): s for s in tkc.free_symbols()} range_preds: dict[Bit, tuple[list[UnitID], int]] = dict() # Apply a rebase to the set of pytket gates which have replacements in qiskit supported_gate_rebase.apply(tkc) for command in tkc: append_tk_command_to_qiskit( command.op, command.args, qcirc, qregmap, cregmap, symb_map, range_preds ) qcirc.global_phase += param_to_qiskit(tkc.phase, symb_map) # if UUID stored in name, set parameter uuids accordingly (see qiskit_to_tk) updates = dict() for p in qcirc.parameters: name_spl = p.name.split("_UUID:", 2) if len(name_spl) == 2: p_name, uuid_str = name_spl uuid = UUID(uuid_str) # See Parameter.__init__() in qiskit/circuit/parameter.py. new_p = Parameter(p_name) new_p._uuid = uuid new_p._parameter_keys = frozenset( ((symengine_wrapper.Symbol(p_name), uuid),) ) new_p._hash = hash((new_p._parameter_keys, new_p._symbol_expr)) updates[p] = new_p qcirc.assign_parameters(updates, inplace=True) return qcirc
[docs] def process_characterisation(backend: "IBMBackend") -> dict[str, Any]: """Convert a :py:class:`qiskit_ibm_runtime.ibm_backend.IBMBackend` to a dictionary containing device Characteristics :param backend: A backend to be converted :return: A dictionary containing device characteristics """ config = backend.configuration() props = backend.properties() return process_characterisation_from_config(config, props)
def process_characterisation_from_config( config: PulseBackendConfiguration, properties: Optional[BackendProperties] ) -> dict[str, Any]: """Obtain a dictionary containing device Characteristics given config and props. :param config: A IBMQ configuration object :param properties: An optional IBMQ properties object :return: A dictionary containing device characteristics """ # TODO explicitly check for and separate 1 and 2 qubit gates def return_value_if_found(iterator: Iterable["Nduv"], name: str) -> Optional[Any]: try: first_found = next(filter(lambda item: item.name == name, iterator)) except StopIteration: return None if hasattr(first_found, "value"): return first_found.value return None coupling_map = config.coupling_map n_qubits = config.n_qubits if coupling_map is None: # Assume full connectivity arc: FullyConnected | Architecture = FullyConnected(n_qubits) else: arc = Architecture(coupling_map) link_errors: dict = defaultdict(dict) node_errors: dict = defaultdict(dict) readout_errors: dict = {} t1_times = [] t2_times = [] frequencies = [] gate_times = [] if properties is not None: for index, qubit_info in enumerate(properties.qubits): t1_times.append([index, return_value_if_found(qubit_info, "T1")]) t2_times.append([index, return_value_if_found(qubit_info, "T2")]) frequencies.append([index, return_value_if_found(qubit_info, "frequency")]) # readout error as a symmetric 2x2 matrix offdiag = return_value_if_found(qubit_info, "readout_error") if offdiag: diag = 1.0 - offdiag readout_errors[index] = [[diag, offdiag], [offdiag, diag]] else: readout_errors[index] = None for gate in properties.gates: name = gate.gate if name in _gate_str_2_optype: optype = _gate_str_2_optype[name] qubits = gate.qubits gate_error = return_value_if_found(gate.parameters, "gate_error") gate_error = gate_error if gate_error else 0.0 gate_length = return_value_if_found(gate.parameters, "gate_length") gate_length = gate_length if gate_length else 0.0 gate_times.append([name, qubits, gate_length]) # add gate fidelities to their relevant lists if len(qubits) == 1: node_errors[qubits[0]].update({optype: gate_error}) elif len(qubits) == 2: link_errors[tuple(qubits)].update({optype: gate_error}) opposite_link = tuple(qubits[::-1]) if opposite_link not in coupling_map: # to simulate a worse reverse direction square the fidelity link_errors[opposite_link].update({optype: 2 * gate_error}) # map type (k1 -> k2) -> v[k1] -> v[k2] K1 = TypeVar("K1") K2 = TypeVar("K2") V = TypeVar("V") convert_keys_t = Callable[[Callable[[K1], K2], dict[K1, V]], dict[K2, V]] # convert qubits to architecture Nodes convert_keys: convert_keys_t = lambda f, d: { # noqa: E731 f(k): v for k, v in d.items() } node_errors = convert_keys(lambda q: Node(q), node_errors) link_errors = convert_keys(lambda p: (Node(p[0]), Node(p[1])), link_errors) readout_errors = convert_keys(lambda q: Node(q), readout_errors) characterisation: dict[str, Any] = dict() characterisation["NodeErrors"] = node_errors characterisation["EdgeErrors"] = link_errors characterisation["ReadoutErrors"] = readout_errors characterisation["Architecture"] = arc characterisation["t1times"] = t1_times characterisation["t2times"] = t2_times characterisation["Frequencies"] = frequencies characterisation["GateTimes"] = gate_times return characterisation def get_avg_characterisation( characterisation: dict[str, Any], ) -> dict[str, dict[Node, float]]: """ Convert gate-specific characterisation into readout, one- and two-qubit errors Used to convert a typical output from `process_characterisation` into an input noise characterisation for NoiseAwarePlacement """ K = TypeVar("K") V1 = TypeVar("V1") V2 = TypeVar("V2") map_values_t = Callable[[Callable[[V1], V2], dict[K, V1]], dict[K, V2]] map_values: map_values_t = lambda f, d: { # noqa: E731 k: f(v) for k, v in d.items() } node_errors = cast(dict[Node, dict[OpType, float]], characterisation["NodeErrors"]) link_errors = cast( dict[tuple[Node, Node], dict[OpType, float]], characterisation["EdgeErrors"] ) readout_errors = cast( dict[Node, list[list[float]]], characterisation["ReadoutErrors"] ) avg: Callable[[dict[Any, float]], float] = lambda xs: sum( # noqa: E731 xs.values() ) / len(xs) avg_mat: Callable[[list[list[float]]], float] = ( # noqa: E731 lambda xs: (xs[0][1] + xs[1][0]) / 2.0 ) avg_readout_errors = map_values(avg_mat, readout_errors) avg_node_errors = map_values(avg, node_errors) avg_link_errors = map_values(avg, link_errors) return { "node_errors": avg_node_errors, "edge_errors": avg_link_errors, "readout_errors": avg_readout_errors, }