# Copyright 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.
import warnings
from collections import defaultdict
from collections.abc import Callable, Iterable
from inspect import signature
from typing import (
TYPE_CHECKING,
Any,
Optional,
TypeVar,
cast,
)
from uuid import UUID
import numpy as np
import sympy
from numpy.typing import NDArray
from pytket.architecture import Architecture, FullyConnected
from pytket.circuit import (
Bit,
CircBox,
Circuit,
Conditional,
Node,
Op,
OpType,
QControlBox,
Qubit,
StatePreparationBox,
Unitary1qBox,
Unitary2qBox,
Unitary3qBox,
UnitType,
)
from pytket.circuit.logic_exp import reg_eq, reg_neq
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_ibm_runtime.models.backend_configuration import ( # type: ignore
QasmBackendConfiguration,
)
from qiskit_ibm_runtime.models.backend_properties import ( # type: ignore
BackendProperties,
)
from symengine import sympify # type: ignore
import qiskit.circuit.library.standard_gates as qiskit_gates # type: ignore
from qiskit import (
ClassicalRegister,
QuantumCircuit,
QuantumRegister,
)
from qiskit.circuit import (
Barrier,
Clbit,
ControlledGate,
Gate,
IfElseOp,
Instruction,
InstructionSet,
Measure,
Parameter,
ParameterExpression,
Reset,
)
from qiskit.circuit import Qubit as QCQubit
from qiskit.circuit.library import (
CRYGate,
Initialize,
PauliEvolutionGate,
RYGate,
StatePreparation,
UnitaryGate,
)
if TYPE_CHECKING:
from pytket.circuit import UnitID
from pytket.unit_id import BitRegister
from qiskit_ibm_runtime.ibm_backend import IBMBackend # type: ignore
from qiskit_ibm_runtime.models.backend_properties import Nduv
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() # noqa: C408
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: QasmBackendConfiguration) -> 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 # noqa: RET504
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, strict=False):
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 :py:class:`~qiskit.circuit.library.Initialize`
and :py:class:`~qiskit.circuit.library.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}. " # noqa: ISC003
+ "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 controlled :py:class:`~pytket.circuit.OpType` from a qiskit :py:class:`~qiskit.circuit.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
if 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
raise NotImplementedError(
"Conversion of qiskit ControlledGate with base gate " # noqa: ISC003
+ f"base gate {c_gate.base_gate}"
+ "not implemented."
)
def _optype_from_qiskit_instruction(instruction: Instruction) -> OpType:
"""Get a pytket :py:class:`~pytket.circuit.OpType` from a qiskit :py:class:`~qiskit.circuit.Instruction`."""
if isinstance(instruction, ControlledGate):
return _get_controlled_tket_optype(instruction)
try:
optype = _known_qiskit_gate[instruction.base_class]
return optype # noqa: RET504
except KeyError:
raise NotImplementedError( # noqa: B904
f"Conversion of qiskit's {instruction.name} instruction is " # noqa: ISC003
+ "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 = f"{c_gate.ctrl_state:b}"
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 :py:class:`~qiskit.circuit.library.Initialize` and :py:class:`~qiskit.circuit.library.StatePreparation`
and appends the appropriate state preparation to a :py:class:`~pytket.circuit.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 = f"{integer_parameter:b}"
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 " # noqa: ISC003
+ "when trying to convert Initialize or StatePreparation instruction."
)
class _CircuitBuilder:
def __init__(
self,
qregs: list[QuantumRegister],
cregs: list[ClassicalRegister] | None = None,
name: str | None = None,
phase: sympy.Expr | None = None,
):
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 = {}
if cregs is not None:
for reg in 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)
@classmethod
def from_qiskit_units(
cls,
qubits: list[QCQubit],
bits: list[Clbit],
name: str | None = None,
phase: sympy.Expr | None = None,
) -> "_CircuitBuilder":
"""Construct a circuit builder from Qiskit's :py:class:`~qiskit.circuit.Qubit` s and :py:class:`~qiskit.circuit.Clbit` s"""
builder = cls([], None, name, phase)
for qb in qubits:
tk_qb = Qubit(qb._register.name, qb._index) # noqa: SLF001
builder.tkc.add_qubit(tk_qb)
builder.qbmap[qb] = tk_qb
for cb in bits:
tk_cb = Bit(cb._register.name, cb._index) # noqa: SLF001
builder.tkc.add_bit(tk_cb)
builder.cbmap[cb] = tk_cb
return builder
def circuit(self) -> Circuit:
return self.tkc
def add_qiskit_data( # noqa: PLR0912
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]
optype = None
if type(instr) not in (PauliEvolutionGate, UnitaryGate, IfElseOp):
# Handling of PauliEvolutionGate, UnitaryGate and IfElseOp 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)
# Note: These IfElseOp/if_test type conditions are only handled
# for single bit conditions and conditions on entire registers.
elif type(instr) is IfElseOp:
_append_if_else_circuit(
if_else_op=instr,
outer_builder=self,
bits=bits,
qargs=qargs,
cargs=cargs,
)
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)),
)
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) # 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,
)
else:
raise NotImplementedError("CUGate with nonzero phase")
else:
params = [_param_to_tk(p) for p in instr.params]
self.tkc.add_gate(
optype, # type: ignore
params,
qubits + bits, # type: ignore
)
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)
def _build_rename_map(
qcirc: QuantumCircuit,
if_else_builder: _CircuitBuilder,
outer_builder: _CircuitBuilder,
qargs: list[QCQubit],
cargs: list[Clbit],
) -> dict[Qubit | Bit, Qubit | Bit]:
rename_map: dict[Qubit | Bit, Qubit | Bit] = {}
for i, inner_q in enumerate(qcirc.qubits):
rename_map[if_else_builder.qbmap[inner_q]] = outer_builder.qbmap[qargs[i]]
for i, inner_c in enumerate(qcirc.clbits):
rename_map[if_else_builder.cbmap[inner_c]] = outer_builder.cbmap[cargs[i]]
return rename_map
# Used for handling of IfElseOp
# docs -> https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.IfElseOp
# Examples -> https://docs.quantum.ibm.com/guides/classical-feedforward-and-control-flow
# pytket-qiskit issue -> https://github.com/CQCL/pytket-qiskit/issues/415
def _pytket_circuits_from_ifelseop(
if_else_op: IfElseOp,
outer_builder: _CircuitBuilder,
qargs: list[QCQubit],
cargs: list[Clbit],
) -> tuple[Circuit, Circuit | None]:
# Extract the QuantumCircuit implementing true_body
if_qc: QuantumCircuit = if_else_op.blocks[0]
# if_qc can have empty qregs, so build from bits
if_builder = _CircuitBuilder.from_qiskit_units(
if_qc.qubits, if_qc.clbits, if_qc.name, _param_to_tk(if_qc.global_phase)
)
if_builder.add_qiskit_data(if_qc)
if_circuit = if_builder.circuit()
# if_circuit might have a different set of registers, which might
# cause problems when appending to the outer circuit.
# We rename the units to make sure the registers in the inner circuit
# is a subset of the registers in the ourter circuit.
if_rename_map = _build_rename_map(
qcirc=if_qc,
if_else_builder=if_builder,
outer_builder=outer_builder,
qargs=qargs,
cargs=cargs,
)
if_circuit.rename_units(if_rename_map) # type: ignore
if_circuit.name = "If"
if_circuit.remove_blank_wires(
keep_blank_classical_wires=False,
remove_classical_only_at_end_of_register=False,
)
# The false_body arg is optional
if len(if_else_op.blocks) == 2: # noqa: PLR2004
else_qc: QuantumCircuit = if_else_op.blocks[1]
else_builder = _CircuitBuilder.from_qiskit_units(
else_qc.qubits,
else_qc.clbits,
else_qc.name,
_param_to_tk(else_qc.global_phase),
)
else_builder.add_qiskit_data(else_qc)
else_circuit = else_builder.circuit()
# else_circuit might have a different set of registers
else_rename_map = _build_rename_map(
else_qc,
if_else_builder=else_builder,
outer_builder=outer_builder,
qargs=qargs,
cargs=cargs,
)
else_circuit.rename_units(else_rename_map) # type: ignore
else_circuit.name = "Else"
else_circuit.remove_blank_wires(
keep_blank_classical_wires=False,
remove_classical_only_at_end_of_register=False,
)
return if_circuit, else_circuit
# If no false_body is specified IfElseOp.blocks is of length 1.
# In this case we return a Circuit implementing true_body and None.
return if_circuit, None
def _append_if_else_circuit(
if_else_op: IfElseOp,
outer_builder: _CircuitBuilder,
bits: list[Bit],
qargs: list[QCQubit],
cargs: list[Clbit],
) -> None:
# Get two pytket circuits which implement the true_body and false_body.
if_circ, else_circ = _pytket_circuits_from_ifelseop(
if_else_op, outer_builder, qargs, cargs
)
# else_circ can be None if no false_body is specified.
if isinstance(if_else_op.condition[0], Clbit):
if len(bits) != 1:
raise NotImplementedError("Conditions on multiple bits not supported")
outer_builder.tkc.add_circbox(
circbox=CircBox(if_circ),
args=if_circ.qubits + if_circ.bits, # type: ignore
condition_bits=bits,
condition_value=if_else_op.condition[1],
)
# If we have an else_circ defined, add it to the circuit
if else_circ is not None:
outer_builder.tkc.add_circbox(
circbox=CircBox(else_circ),
args=else_circ.qubits + else_circ.bits, # type: ignore
condition_bits=bits,
condition_value=1 ^ if_else_op.condition[1],
)
elif isinstance(if_else_op.condition[0], ClassicalRegister):
pytket_bit_reg: BitRegister = outer_builder.tkc.get_c_register(
if_else_op.condition[0].name
)
outer_builder.tkc.add_circbox(
circbox=CircBox(if_circ),
args=if_circ.qubits + if_circ.bits, # type: ignore
condition=reg_eq(pytket_bit_reg, if_else_op.condition[1]),
)
if else_circ is not None:
outer_builder.tkc.add_circbox(
circbox=CircBox(else_circ),
args=else_circ.qubits + else_circ.bits, # type: ignore
condition=reg_neq(pytket_bit_reg, if_else_op.condition[1]),
)
else:
raise TypeError(
"Unrecognized type used to construct IfElseOp. Expected " # noqa: ISC003
+ f"ClBit or ClassicalRegister, got {type(if_else_op.condition[0])}"
)
[docs]
def qiskit_to_tk(qcirc: QuantumCircuit, preserve_param_uuid: bool = False) -> Circuit:
"""
Converts a qiskit :py:class:`qiskit.circuit.QuantumCircuit` to a pytket :py:class:`~pytket.circuit.Circuit`.
*Note:* Support for conversion of symbolic circuits is currently limited. In
particular, if the circuit contains `ParameterVectorElement` symbols this function
will probably fail.
:param qcirc: A circuit to be converted
:param preserve_param_uuid: Whether to preserve symbolic :py:class:`~qiskit.circuit.Parameter` uuids
by appending them to the tket :py:class:`~pytket.circuit.Circuit` symbol names as "_UUID:<uuid_as_hex>".
This can be useful if you want to reassign :py:class:`~qiskit.circuit.Parameter` s after conversion
to tket and back, as it is necessary for :py:class:`~qiskit.circuit.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.hex}") 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):
return sympy.sympify(str(p)) / sympy.pi
return p / sympy.pi
def _param_to_qiskit(
p: sympy.Expr, symb_map: dict[Parameter, sympy.Symbol]
) -> float | ParameterExpression | Parameter:
ppi = p * sympy.pi
if len(ppi.free_symbols) == 0:
return float(ppi.evalf())
return Parameter(str(sympify(ppi)))
def _get_params(
op: Op, symb_map: dict[Parameter, sympy.Symbol]
) -> list[float | ParameterExpression | Parameter]:
return [_param_to_qiskit(p, symb_map) for p in op.params]
def _apply_qiskit_instruction(
qcirc: QuantumCircuit,
instruc: Instruction,
qargs: Iterable[UnitType.qubit], # type: ignore
cargs: Iterable[Clbit] = None, # type: ignore # noqa: RUF013
condition: tuple[ClassicalRegister | Clbit, int] | None = None,
) -> None:
if condition is None:
qcirc.append(instruc, qargs, cargs)
else:
with qcirc.if_test(condition):
qcirc.append(instruc, qargs, cargs)
def _has_if_else(qc: QuantumCircuit) -> bool:
"""Check if a :py:class:`~qiskit.circuit.QuantumCircuit` contains an :py:class:`~qiskit.circuit.IfElseOp`."""
return "if_else" in qc.count_ops()
[docs]
def append_tk_command_to_qiskit( # noqa: PLR0911, PLR0912, PLR0913, PLR0915
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]],
condition: tuple[ClassicalRegister | Clbit, int] | None = None,
) -> 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]]
# If the bit is storing a range predicate it should be invalidated:
range_preds.pop(bit, None) # type: ignore
_apply_qiskit_instruction(qcirc, Measure(), [qb], [b], condition)
return qcirc
if optype == OpType.Reset:
qb = qregmap[args[0].reg_name][args[0].index[0]]
_apply_qiskit_instruction(qcirc, Reset(), qargs=[qb], condition=condition)
return qcirc
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()
_apply_qiskit_instruction(
qcirc=qcirc, instruc=instruc, qargs=qargs, condition=condition
)
elif _has_if_else(subqc):
# Detect control flow in CircBoxes and raise an error.
raise NotImplementedError(
"Conversion of CircBox(es) containing conditional" # noqa: ISC003
+ " gates not currently supported by tk_to_qiskit"
)
else:
instruc = subqc.to_instruction()
_apply_qiskit_instruction(
qcirc=qcirc, instruc=instruc, qargs=qargs, condition=condition
)
return qcirc
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
unitary_gate = 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).
_apply_qiskit_instruction(
qcirc, unitary_gate, qargs=list(reversed(qargs)), condition=condition
)
return qcirc
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)
_apply_qiskit_instruction(
qcirc=qcirc,
instruc=initializer,
qargs=list(reversed(qargs)),
condition=condition,
)
return qcirc
qiskit_state_prep_box = StatePreparation(statevector_array)
_apply_qiskit_instruction(
qcirc=qcirc,
instruc=qiskit_state_prep_box,
qargs=list(reversed(qargs)),
condition=condition,
)
return qcirc
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( # noqa: B904
"Conversion of QControlBox with base gate" # noqa: ISC003
+ f"{op.get_op()} not supported by tk_to_qiskit."
)
params = _get_params(op.get_op(), symb_map)
operation = gatetype(*params)
_apply_qiskit_instruction(
qcirc=qcirc,
instruc=operation.control(
num_ctrl_qubits=op.get_n_controls(), ctrl_state=qiskit_control_state
),
qargs=qargs,
condition=condition,
)
return qcirc
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]
barr = Barrier(len(args))
_apply_qiskit_instruction(qcirc, instruc=barr, qargs=qargs, condition=condition)
return qcirc
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:
assert isinstance(op, Conditional)
if op.op.type == OpType.Conditional:
# See https://github.com/CQCL/pytket-qiskit/issues/442
raise NotImplementedError("Nested conditional not supported")
if op.op.type == OpType.Phase:
# conditional phase not supported
return InstructionSet()
if args[0] in range_preds:
assert op.value == 1
condition_bits, value = range_preds[args[0]] # type: ignore
args = condition_bits + args[1:]
width = len(condition_bits)
else:
width = op.width
value = op.value
regname = args[0].reg_name
for i, a in enumerate(args[:width]): # noqa: B007
if a.reg_name != regname:
raise NotImplementedError("Conditions can only use a single register")
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"""
)
condition = (cregmap[regname], value)
elif width == 1:
condition = (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"""
)
instruction = append_tk_command_to_qiskit(
op.op,
args[width:],
qcirc,
qregmap,
cregmap,
symb_map,
range_preds,
condition=condition,
)
return instruction # noqa: RET504
# normal gates
qargs = [qregmap[q.reg_name][q.index[0]] for q in args]
if optype == OpType.CnX:
_apply_qiskit_instruction(
qcirc,
qiskit_gates.MCXGate(num_ctrl_qubits=len(qargs) - 1),
qargs=qargs,
condition=condition,
)
return qcirc
if optype == OpType.CnY:
_apply_qiskit_instruction(
qcirc=qcirc,
instruc=qiskit_gates.YGate().control(len(qargs) - 1),
qargs=qargs,
condition=condition,
)
return qcirc
if optype == OpType.CnZ:
if len(qargs) == 2: # noqa: PLR2004
z_gate = qiskit_gates.CZGate()
else:
z_gate = qiskit_gates.ZGate().control(len(qargs) - 1)
z_gate.name = "mcz"
_apply_qiskit_instruction(
qcirc=qcirc, instruc=z_gate, qargs=qargs, condition=condition
)
return qcirc
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 # noqa: PLR2004
if len(qargs) == 2: # noqa: PLR2004
# presumably more efficient; single control only
new_gate = CRYGate(alpha)
else:
new_gate = RYGate(alpha).control(len(qargs) - 1)
_apply_qiskit_instruction(qcirc, new_gate, qargs=qargs, condition=condition)
return qcirc
if optype == OpType.CU3:
params = _get_params(op, symb_map) + [0] # noqa: RUF005
_apply_qiskit_instruction(
qcirc, qiskit_gates.CUGate(*params), qargs=qargs, condition=condition
)
return qcirc
if optype == OpType.TK1:
params = _get_params(op, symb_map)
half = np.pi / 2
qcirc.global_phase += -params[0] / 2 - params[2] / 2
_apply_qiskit_instruction(
qcirc,
qiskit_gates.UGate(params[1], params[0] - half, params[2] + half),
qargs=qargs,
condition=condition,
)
return qcirc
if optype == OpType.Phase:
params = _get_params(op, symb_map)
assert len(params) == 1
# TODO is there a way to make this conditional?
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)
_apply_qiskit_instruction(
qcirc=qcirc,
instruc=g,
qargs=qargs,
condition=condition,
)
return qcirc
# 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)
[docs]
def tk_to_qiskit(
tkcirc: Circuit,
replace_implicit_swaps: bool = False,
perm_warning: bool = True,
) -> QuantumCircuit:
"""
Converts a pytket :py:class:`~pytket.circuit.Circuit` to a qiskit :py:class:`qiskit.circuit.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 :py:class:`~pytket.circuit.Circuit` are not handled by default.
Consider using the ``replace_implicit_swaps`` flag to replace these implicit swaps with
SWAP gates.
*Note:* Support for conversion of symbolic circuits is currently limited.
:param tkcirc: A :py:class:`~pytket.circuit.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 tkcirc.has_implicit_wireswaps and perm_warning and not replace_implicit_swaps:
warnings.warn( # noqa: B028
"The pytket Circuit contains implicit qubit permutations" # noqa: ISC003
+ " which aren't handled by default."
+ " Consider using the replace_implicit_swaps flag in tk_to_qiskit or"
+ " replacing them using Circuit.replace_implicit_wire_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): # noqa: C401
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() # noqa: C408
# 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() # noqa: C408
for p in qcirc.parameters:
name_spl = p.name.split("_UUID_", 2)
if len(name_spl) == 2: # noqa: PLR2004
p_name, uuid_str = name_spl
uuid = UUID(uuid_str)
# See Parameter.__init__() in qiskit/circuit/parameter.py.
new_p = Parameter(p_name, uuid=uuid)
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)
[docs]
def process_characterisation_from_config( # noqa: PLR0915
config: QasmBackendConfiguration, properties: BackendProperties | None
) -> 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) -> Any | None:
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: # noqa: PLR2004
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: {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() # noqa: C408
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
[docs]
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 :py:func:`~.process_characterisation` into an input
noise characterisation for :py:class:`~pytket.placement.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: {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(xs.values()) / len(xs)
avg_mat: Callable[[list[list[float]]], float] = (
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,
}