From 519a5123e4c0a4ff93c7077b3cb2f827522ebe87 Mon Sep 17 00:00:00 2001 From: yoshi-qc <95134786+yoshi-qc@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:24:07 +0900 Subject: [PATCH] Replace CnY and CnZ in qiskit gates Fixed the bug for https://github.com/CQCL/pytket-qiskit/issues/368 --- pytket/extensions/qiskit/qiskit_convert.py | 2012 ++++++++++---------- 1 file changed, 1008 insertions(+), 1004 deletions(-) diff --git a/pytket/extensions/qiskit/qiskit_convert.py b/pytket/extensions/qiskit/qiskit_convert.py index 69edbc53..e44a6c72 100644 --- a/pytket/extensions/qiskit/qiskit_convert.py +++ b/pytket/extensions/qiskit/qiskit_convert.py @@ -1,1004 +1,1008 @@ -# 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 -""" -from collections import defaultdict -from typing import ( - Callable, - Dict, - List, - Optional, - Union, - Any, - Iterable, - cast, - Set, - Tuple, - TypeVar, - TYPE_CHECKING, -) -from inspect import signature -from uuid import UUID - -import numpy as np -from symengine import sympify # type: ignore - -import sympy -import qiskit.circuit.library.standard_gates as qiskit_gates # type: ignore -from qiskit import ( - ClassicalRegister, - QuantumCircuit, - QuantumRegister, -) -from qiskit.circuit import ( - Barrier, - Instruction, - InstructionSet, - Gate, - ControlledGate, - Measure, - Parameter, - ParameterExpression, - Reset, - Clbit, -) -from qiskit.circuit.library import ( - CRYGate, - RYGate, - PauliEvolutionGate, - StatePreparation, - UnitaryGate, - Initialize, -) -from qiskit.providers.models import BackendProperties, QasmBackendConfiguration # type: ignore - -from pytket.circuit import ( - CircBox, - Circuit, - Node, - Op, - OpType, - Unitary1qBox, - Unitary2qBox, - Unitary3qBox, - UnitType, - Bit, - Qubit, - QControlBox, - StatePreparationBox, -) -from pytket.unit_id import _TEMP_BIT_NAME -from pytket.pauli import Pauli, QubitPauliString -from pytket.architecture import Architecture, FullyConnected -from pytket.utils import QubitPauliOperator, gen_term_sequence_circuit -from pytket.passes import RebaseCustom - -if TYPE_CHECKING: - from qiskit.providers.backend import BackendV1 # type: ignore - from qiskit.providers.models.backendproperties import Nduv # type: ignore - from qiskit.circuit.quantumcircuitdata import QuantumCircuitData # type: ignore - from pytket.circuit import Op, UnitID - -_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: 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 - - 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("Coefficient for Pauli {} is non-real.".format(p)) - 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_instruction: Instruction -) -> Circuit: - """Helper function to handle strings in QuantumCircuit.initialize - and QuantumCircuit.prepare_state""" - - circ = Circuit(n_qubits) - # Check if Instruction is Initialize or Statepreparation - # If Initialize, add resets - if isinstance(qiskit_instruction, Initialize): - for qubit in circ.qubits: - circ.add_gate(OpType.Reset, [qubit]) - - # We iterate through the string in reverse to add the - # gates in the correct order (endian-ness). - for count, char in enumerate(reversed(circuit_string)): - if char == "0": - pass - elif char == "1": - circ.X(count) - elif char == "+": - circ.H(count) - elif char == "-": - circ.X(count) - circ.H(count) - elif char == "r": - circ.H(count) - circ.S(count) - elif char == "l": - circ.H(count) - circ.Sdg(count) - else: - raise ValueError( - f"Cannot parse string for character {char}. " - + "The supported characters are {'0', '1', '+', '-', 'r', 'l'}." - ) - - return circ - - -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_xs( - self, - num_ctrl_qubits: Optional[int], - ctrl_state: Optional[Union[str, int]], - qargs: List["Qubit"], - ) -> None: - if ctrl_state is not None: - assert isinstance(num_ctrl_qubits, int) - assert num_ctrl_qubits >= 0 - c = int(ctrl_state, 2) if isinstance(ctrl_state, str) else int(ctrl_state) - assert c >= 0 and (c >> num_ctrl_qubits) == 0 - for i in range(num_ctrl_qubits): - if ((c >> i) & 1) == 0: - self.tkc.X(self.qbmap[qargs[i]]) - - def add_qiskit_data( - self, circuit: QuantumCircuit, data: Optional["QuantumCircuitData"] = None - ) -> None: - data = data or circuit.data - for instr, qargs, cargs in data: - condition_kwargs = {} - if instr.condition is not None: - if type(instr.condition[0]) == ClassicalRegister: - cond_reg = self.cregmap[instr.condition[0]] - condition_kwargs = { - "condition_bits": [cond_reg[k] for k in range(len(cond_reg))], - "condition_value": instr.condition[1], - } - elif type(instr.condition[0]) == Clbit: - # .find_bit() returns type: - # tuple[index, list[tuple[ClassicalRegister, index]]] - # We assume each bit belongs to exactly one register. - index = circuit.find_bit(instr.condition[0])[0] - register = circuit.find_bit(instr.condition[0])[1][0][0] - cond_reg = self.cregmap[register] - condition_kwargs = { - "condition_bits": [cond_reg[index]], - "condition_value": instr.condition[1], - } - else: - raise NotImplementedError( - "condition must contain classical bit or register" - ) - - # Controlled operations may be controlled on values other than all-1. Handle - # this by prepending and appending X gates on the control qubits. - ctrl_state, num_ctrl_qubits = None, None - try: - ctrl_state = instr.ctrl_state - num_ctrl_qubits = instr.num_ctrl_qubits - except AttributeError: - pass - self.add_xs(num_ctrl_qubits, ctrl_state, qargs) - optype = None - if isinstance(instr, ControlledGate): - if instr.base_class in _known_qiskit_gate: - # First we check if the gate is in _known_qiskit_gate - # this avoids CZ being converted to CnZ - optype = _known_qiskit_gate[instr.base_class] - elif instr.base_gate.base_class is qiskit_gates.RYGate: - optype = OpType.CnRy - elif instr.base_gate.base_class is qiskit_gates.YGate: - optype = OpType.CnY - elif instr.base_gate.base_class is qiskit_gates.ZGate: - optype = OpType.CnZ - else: - if instr.base_gate.base_class in _known_qiskit_gate: - optype = OpType.QControlBox # QControlBox case handled below - else: - raise NotImplementedError( - f"qiskit ControlledGate with base gate {instr.base_gate}" - + "not implemented" - ) - elif type(instr) in [PauliEvolutionGate, UnitaryGate]: - pass # Special handling below - else: - try: - optype = _known_qiskit_gate[instr.base_class] - except KeyError: - raise NotImplementedError( - f"Conversion of qiskit's {instr.name} instruction is " - + "currently unsupported by qiskit_to_tk. Consider " - + "using QuantumCircuit.decompose() before attempting " - + "conversion." - ) - qubits = [self.qbmap[qbit] for qbit in qargs] - bits = [self.cbmap[bit] for bit in cargs] - - if optype == OpType.QControlBox: - base_tket_gate = _known_qiskit_gate[instr.base_gate.base_class] - params = [param_to_tk(p) for p in instr.base_gate.params] - n_base_qubits = instr.base_gate.num_qubits - sub_circ = Circuit(n_base_qubits) - # use base gate name for the CircBox (shows in renderer) - sub_circ.name = instr.base_gate.name.capitalize() - sub_circ.add_gate(base_tket_gate, params, list(range(n_base_qubits))) - c_box = CircBox(sub_circ) - q_ctrl_box = QControlBox(c_box, instr.num_ctrl_qubits) - self.tkc.add_qcontrolbox(q_ctrl_box, qubits) - - elif isinstance(instr, (Initialize, StatePreparation)): - # Check how Initialize or StatePrep is constructed - if isinstance(instr.params[0], str): - # Parse string to get the right single qubit gates - circuit_string = "".join(instr.params) - circuit = _string_to_circuit( - circuit_string, instr.num_qubits, qiskit_instruction=instr - ) - self.tkc.add_circuit(circuit, qubits) - - elif isinstance(instr.params, list) and len(instr.params) != 1: - amplitude_list = instr.params - if isinstance(instr, Initialize): - pytket_state_prep_box = StatePreparationBox( - amplitude_list, with_initial_reset=True # type: ignore - ) - else: - pytket_state_prep_box = StatePreparationBox( - amplitude_list, with_initial_reset=False # type: ignore - ) - # Need to reverse qubits here (endian-ness) - reversed_qubits = list(reversed(qubits)) - self.tkc.add_gate(pytket_state_prep_box, reversed_qubits) - - elif isinstance(instr.params[0], complex) and len(instr.params) == 1: - # convert int to a binary string and apply X for |1> - integer_parameter = int(instr.params[0].real) - bit_string = bin(integer_parameter)[2:] - circuit = _string_to_circuit( - bit_string, instr.num_qubits, qiskit_instruction=instr - ) - self.tkc.add_circuit(circuit, qubits) - - elif type(instr) == 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) == UnitaryGate: - # Note reversal of qubits, to account for endianness (pytket unitaries - # are ILO-BE == DLO-LE; qiskit unitaries are ILO-LE == DLO-BE). - params = instr.params - assert len(params) == 1 - u = cast(np.ndarray, params[0]) - assert len(cargs) == 0 - n = len(qubits) - if n == 0: - assert u.shape == (1, 1) - self.tkc.add_phase(np.angle(u[0][0]) / np.pi) - elif n == 1: - assert u.shape == (2, 2) - u1box = Unitary1qBox(u) - self.tkc.add_unitary1qbox(u1box, qubits[0], **condition_kwargs) - elif n == 2: - assert u.shape == (4, 4) - u2box = Unitary2qBox(u) - self.tkc.add_unitary2qbox( - u2box, qubits[1], qubits[0], **condition_kwargs - ) - elif n == 3: - assert u.shape == (8, 8) - u3box = Unitary3qBox(u) - self.tkc.add_unitary3qbox( - u3box, qubits[2], qubits[1], qubits[0], **condition_kwargs - ) - else: - raise NotImplementedError( - f"Conversion of {n}-qubit unitary gates not supported" - ) - - elif optype == OpType.Barrier: - self.tkc.add_barrier(qubits) - elif optype == OpType.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 - self.tkc.add_circbox(CircBox(subc), qubits + bits, **condition_kwargs) # type: ignore - - elif optype == OpType.CU3 and type(instr) == 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 - - self.add_xs(num_ctrl_qubits, ctrl_state, qargs) - - -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 - :type qcirc: QuantumCircuit - :param preserve_param_uuid: Whether to preserve symbolic Parameter uuids - by appending them to the tket Circuit symbol names as "_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. - :type preserve_param_uuid: bool - :return: The converted circuit - :rtype: 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 param_to_tk(p: Union[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] -) -> Union[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[Union[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.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, args[width:], qcirc, qregmap, cregmap, symb_map, range_preds # type: ignore - ) - 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: - return qcirc.append(qiskit_gates.ZGate().control(len(qargs) - 1), 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) == float: - qcirc.global_phase += phase * np.pi - else: - qcirc.global_phase += sympify(phase * sympy.pi) - return qcirc.append(g, qargs=qargs) - - -# Define varibles for RebaseCustom -_cx_replacement = Circuit(2).CX(0, 1) - -# 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.CustomGate} -) - - -Param = Union[float, "sympy.Expr"] # Type for TK1 and U3 parameters - - -# Use the U3 gate for tk1_replacement as this is a member of _supported_tket_gates -def _tk1_to_u3(a: Param, b: Param, c: Param) -> Circuit: - tk1_circ = Circuit(1) - tk1_circ.add_gate(OpType.U3, [b, a - 1 / 2, c + 1 / 2], [0]).add_phase(-(a + c) / 2) - return tk1_circ - - -# This is a rebase to the set of tket gates which have an exact substitution in qiskit -supported_gate_rebase = RebaseCustom(_protected_tket_gates, _cx_replacement, _tk1_to_u3) - - -def tk_to_qiskit( - tkcirc: Circuit, replace_implicit_swaps: bool = False -) -> 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. - - :param tkcirc: A :py:class:`Circuit` to be converted - :type tkcirc: Circuit - :param replace_implicit_swaps: Implement implicit permutation by adding SWAPs - to the end of the circuit. - :type replace_implicit_swaps: bool - :return: The converted circuit - :rtype: QuantumCircuit - """ - tkc = tkcirc.copy() # Make a local copy of tkcirc - if replace_implicit_swaps: - tkc.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): - 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(((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 - - -def process_characterisation(backend: "BackendV1") -> Dict[str, Any]: - """Convert a :py:class:`qiskit.providers.backend.BackendV1` to a dictionary - containing device Characteristics - - :param backend: A backend to be converted - :type backend: BackendV1 - :return: A dictionary containing device characteristics - :rtype: dict - """ - config = backend.configuration() - props = backend.properties() - return process_characterisation_from_config(config, props) - - -def process_characterisation_from_config( - config: QasmBackendConfiguration, properties: Optional[BackendProperties] -) -> Dict[str, Any]: - """Obtain a dictionary containing device Characteristics given config and props. - - :param config: A IBMQ configuration object - :type config: QasmBackendConfiguration - :param properties: An optional IBMQ properties object - :type properties: Optional[BackendProperties] - :return: A dictionary containing device characteristics - :rtype: dict - """ - - # 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: Union[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: {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: {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, - } +# 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 +""" +from collections import defaultdict +from typing import ( + Callable, + Dict, + List, + Optional, + Union, + Any, + Iterable, + cast, + Set, + Tuple, + TypeVar, + TYPE_CHECKING, +) +from inspect import signature +from uuid import UUID + +import numpy as np +from symengine import sympify # type: ignore + +import sympy +import qiskit.circuit.library.standard_gates as qiskit_gates # type: ignore +from qiskit import ( + ClassicalRegister, + QuantumCircuit, + QuantumRegister, +) +from qiskit.circuit import ( + Barrier, + Instruction, + InstructionSet, + Gate, + ControlledGate, + Measure, + Parameter, + ParameterExpression, + Reset, + Clbit, +) +from qiskit.circuit.library import ( + CRYGate, + RYGate, + PauliEvolutionGate, + StatePreparation, + UnitaryGate, + Initialize, +) +from qiskit.providers.models import BackendProperties, QasmBackendConfiguration # type: ignore + +from pytket.circuit import ( + CircBox, + Circuit, + Node, + Op, + OpType, + Unitary1qBox, + Unitary2qBox, + Unitary3qBox, + UnitType, + Bit, + Qubit, + QControlBox, + StatePreparationBox, +) +from pytket.unit_id import _TEMP_BIT_NAME +from pytket.pauli import Pauli, QubitPauliString +from pytket.architecture import Architecture, FullyConnected +from pytket.utils import QubitPauliOperator, gen_term_sequence_circuit +from pytket.passes import RebaseCustom + +if TYPE_CHECKING: + from qiskit.providers.backend import BackendV1 # type: ignore + from qiskit.providers.models.backendproperties import Nduv # type: ignore + from qiskit.circuit.quantumcircuitdata import QuantumCircuitData # type: ignore + from pytket.circuit import Op, UnitID + +_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: 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 + + 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("Coefficient for Pauli {} is non-real.".format(p)) + 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_instruction: Instruction +) -> Circuit: + """Helper function to handle strings in QuantumCircuit.initialize + and QuantumCircuit.prepare_state""" + + circ = Circuit(n_qubits) + # Check if Instruction is Initialize or Statepreparation + # If Initialize, add resets + if isinstance(qiskit_instruction, Initialize): + for qubit in circ.qubits: + circ.add_gate(OpType.Reset, [qubit]) + + # We iterate through the string in reverse to add the + # gates in the correct order (endian-ness). + for count, char in enumerate(reversed(circuit_string)): + if char == "0": + pass + elif char == "1": + circ.X(count) + elif char == "+": + circ.H(count) + elif char == "-": + circ.X(count) + circ.H(count) + elif char == "r": + circ.H(count) + circ.S(count) + elif char == "l": + circ.H(count) + circ.Sdg(count) + else: + raise ValueError( + f"Cannot parse string for character {char}. " + + "The supported characters are {'0', '1', '+', '-', 'r', 'l'}." + ) + + return circ + + +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_xs( + self, + num_ctrl_qubits: Optional[int], + ctrl_state: Optional[Union[str, int]], + qargs: List["Qubit"], + ) -> None: + if ctrl_state is not None: + assert isinstance(num_ctrl_qubits, int) + assert num_ctrl_qubits >= 0 + c = int(ctrl_state, 2) if isinstance(ctrl_state, str) else int(ctrl_state) + assert c >= 0 and (c >> num_ctrl_qubits) == 0 + for i in range(num_ctrl_qubits): + if ((c >> i) & 1) == 0: + self.tkc.X(self.qbmap[qargs[i]]) + + def add_qiskit_data( + self, circuit: QuantumCircuit, data: Optional["QuantumCircuitData"] = None + ) -> None: + data = data or circuit.data + for instr, qargs, cargs in data: + condition_kwargs = {} + if instr.condition is not None: + if type(instr.condition[0]) == ClassicalRegister: + cond_reg = self.cregmap[instr.condition[0]] + condition_kwargs = { + "condition_bits": [cond_reg[k] for k in range(len(cond_reg))], + "condition_value": instr.condition[1], + } + elif type(instr.condition[0]) == Clbit: + # .find_bit() returns type: + # tuple[index, list[tuple[ClassicalRegister, index]]] + # We assume each bit belongs to exactly one register. + index = circuit.find_bit(instr.condition[0])[0] + register = circuit.find_bit(instr.condition[0])[1][0][0] + cond_reg = self.cregmap[register] + condition_kwargs = { + "condition_bits": [cond_reg[index]], + "condition_value": instr.condition[1], + } + else: + raise NotImplementedError( + "condition must contain classical bit or register" + ) + + # Controlled operations may be controlled on values other than all-1. Handle + # this by prepending and appending X gates on the control qubits. + ctrl_state, num_ctrl_qubits = None, None + try: + ctrl_state = instr.ctrl_state + num_ctrl_qubits = instr.num_ctrl_qubits + except AttributeError: + pass + self.add_xs(num_ctrl_qubits, ctrl_state, qargs) + optype = None + if isinstance(instr, ControlledGate): + if instr.base_class in _known_qiskit_gate: + # First we check if the gate is in _known_qiskit_gate + # this avoids CZ being converted to CnZ + optype = _known_qiskit_gate[instr.base_class] + elif instr.base_gate.base_class is qiskit_gates.RYGate: + optype = OpType.CnRy + elif instr.base_gate.base_class is qiskit_gates.YGate: + optype = OpType.CnY + elif instr.base_gate.base_class is qiskit_gates.ZGate: + optype = OpType.CnZ + else: + if instr.base_gate.base_class in _known_qiskit_gate: + optype = OpType.QControlBox # QControlBox case handled below + else: + raise NotImplementedError( + f"qiskit ControlledGate with base gate {instr.base_gate}" + + "not implemented" + ) + elif type(instr) in [PauliEvolutionGate, UnitaryGate]: + pass # Special handling below + else: + try: + optype = _known_qiskit_gate[instr.base_class] + except KeyError: + raise NotImplementedError( + f"Conversion of qiskit's {instr.name} instruction is " + + "currently unsupported by qiskit_to_tk. Consider " + + "using QuantumCircuit.decompose() before attempting " + + "conversion." + ) + qubits = [self.qbmap[qbit] for qbit in qargs] + bits = [self.cbmap[bit] for bit in cargs] + + if optype == OpType.QControlBox: + base_tket_gate = _known_qiskit_gate[instr.base_gate.base_class] + params = [param_to_tk(p) for p in instr.base_gate.params] + n_base_qubits = instr.base_gate.num_qubits + sub_circ = Circuit(n_base_qubits) + # use base gate name for the CircBox (shows in renderer) + sub_circ.name = instr.base_gate.name.capitalize() + sub_circ.add_gate(base_tket_gate, params, list(range(n_base_qubits))) + c_box = CircBox(sub_circ) + q_ctrl_box = QControlBox(c_box, instr.num_ctrl_qubits) + self.tkc.add_qcontrolbox(q_ctrl_box, qubits) + + elif isinstance(instr, (Initialize, StatePreparation)): + # Check how Initialize or StatePrep is constructed + if isinstance(instr.params[0], str): + # Parse string to get the right single qubit gates + circuit_string = "".join(instr.params) + circuit = _string_to_circuit( + circuit_string, instr.num_qubits, qiskit_instruction=instr + ) + self.tkc.add_circuit(circuit, qubits) + + elif isinstance(instr.params, list) and len(instr.params) != 1: + amplitude_list = instr.params + if isinstance(instr, Initialize): + pytket_state_prep_box = StatePreparationBox( + amplitude_list, with_initial_reset=True # type: ignore + ) + else: + pytket_state_prep_box = StatePreparationBox( + amplitude_list, with_initial_reset=False # type: ignore + ) + # Need to reverse qubits here (endian-ness) + reversed_qubits = list(reversed(qubits)) + self.tkc.add_gate(pytket_state_prep_box, reversed_qubits) + + elif isinstance(instr.params[0], complex) and len(instr.params) == 1: + # convert int to a binary string and apply X for |1> + integer_parameter = int(instr.params[0].real) + bit_string = bin(integer_parameter)[2:] + circuit = _string_to_circuit( + bit_string, instr.num_qubits, qiskit_instruction=instr + ) + self.tkc.add_circuit(circuit, qubits) + + elif type(instr) == 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) == UnitaryGate: + # Note reversal of qubits, to account for endianness (pytket unitaries + # are ILO-BE == DLO-LE; qiskit unitaries are ILO-LE == DLO-BE). + params = instr.params + assert len(params) == 1 + u = cast(np.ndarray, params[0]) + assert len(cargs) == 0 + n = len(qubits) + if n == 0: + assert u.shape == (1, 1) + self.tkc.add_phase(np.angle(u[0][0]) / np.pi) + elif n == 1: + assert u.shape == (2, 2) + u1box = Unitary1qBox(u) + self.tkc.add_unitary1qbox(u1box, qubits[0], **condition_kwargs) + elif n == 2: + assert u.shape == (4, 4) + u2box = Unitary2qBox(u) + self.tkc.add_unitary2qbox( + u2box, qubits[1], qubits[0], **condition_kwargs + ) + elif n == 3: + assert u.shape == (8, 8) + u3box = Unitary3qBox(u) + self.tkc.add_unitary3qbox( + u3box, qubits[2], qubits[1], qubits[0], **condition_kwargs + ) + else: + raise NotImplementedError( + f"Conversion of {n}-qubit unitary gates not supported" + ) + + elif optype == OpType.Barrier: + self.tkc.add_barrier(qubits) + elif optype == OpType.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 + self.tkc.add_circbox(CircBox(subc), qubits + bits, **condition_kwargs) # type: ignore + + elif optype == OpType.CU3 and type(instr) == 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 + + self.add_xs(num_ctrl_qubits, ctrl_state, qargs) + + +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 + :type qcirc: QuantumCircuit + :param preserve_param_uuid: Whether to preserve symbolic Parameter uuids + by appending them to the tket Circuit symbol names as "_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. + :type preserve_param_uuid: bool + :return: The converted circuit + :rtype: 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 param_to_tk(p: Union[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] +) -> Union[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[Union[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.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, args[width:], qcirc, qregmap, cregmap, symb_map, range_preds # type: ignore + ) + 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: + qcirc.s(qargs[-1]) + qcirc.mcx(qargs[:-1], qargs[-1]) + return qcirc.sdg(qargs[-1]) + if optype == OpType.CnZ: + qcirc.h(qargs[-1]) + qcirc.mcx(qargs[:-1], qargs[-1]) + return qcirc.h(qargs[-1]) + 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) == float: + qcirc.global_phase += phase * np.pi + else: + qcirc.global_phase += sympify(phase * sympy.pi) + return qcirc.append(g, qargs=qargs) + + +# Define varibles for RebaseCustom +_cx_replacement = Circuit(2).CX(0, 1) + +# 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.CustomGate} +) + + +Param = Union[float, "sympy.Expr"] # Type for TK1 and U3 parameters + + +# Use the U3 gate for tk1_replacement as this is a member of _supported_tket_gates +def _tk1_to_u3(a: Param, b: Param, c: Param) -> Circuit: + tk1_circ = Circuit(1) + tk1_circ.add_gate(OpType.U3, [b, a - 1 / 2, c + 1 / 2], [0]).add_phase(-(a + c) / 2) + return tk1_circ + + +# This is a rebase to the set of tket gates which have an exact substitution in qiskit +supported_gate_rebase = RebaseCustom(_protected_tket_gates, _cx_replacement, _tk1_to_u3) + + +def tk_to_qiskit( + tkcirc: Circuit, replace_implicit_swaps: bool = False +) -> 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. + + :param tkcirc: A :py:class:`Circuit` to be converted + :type tkcirc: Circuit + :param replace_implicit_swaps: Implement implicit permutation by adding SWAPs + to the end of the circuit. + :type replace_implicit_swaps: bool + :return: The converted circuit + :rtype: QuantumCircuit + """ + tkc = tkcirc.copy() # Make a local copy of tkcirc + if replace_implicit_swaps: + tkc.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): + 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(((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 + + +def process_characterisation(backend: "BackendV1") -> Dict[str, Any]: + """Convert a :py:class:`qiskit.providers.backend.BackendV1` to a dictionary + containing device Characteristics + + :param backend: A backend to be converted + :type backend: BackendV1 + :return: A dictionary containing device characteristics + :rtype: dict + """ + config = backend.configuration() + props = backend.properties() + return process_characterisation_from_config(config, props) + + +def process_characterisation_from_config( + config: QasmBackendConfiguration, properties: Optional[BackendProperties] +) -> Dict[str, Any]: + """Obtain a dictionary containing device Characteristics given config and props. + + :param config: A IBMQ configuration object + :type config: QasmBackendConfiguration + :param properties: An optional IBMQ properties object + :type properties: Optional[BackendProperties] + :return: A dictionary containing device characteristics + :rtype: dict + """ + + # 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: Union[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: {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: {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, + }