Source code for pytket.extensions.pyquil.pyquil_convert

# Copyright 2019-2024 Quantinuum
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Methods to allow conversion between pyQuil and tket data types
"""

from collections import defaultdict
from logging import warning
import math
from typing import (
    Any,
    Callable,
    Union,
    Dict,
    List,
    Optional,
    Tuple,
    TypeVar,
    cast,
    overload,
)
from typing_extensions import Literal

from pyquil import Program
from pyquil.api import QuantumComputer
from pyquil.external.rpcq import GateInfo, MeasureInfo
from pyquil.quilatom import (
    Qubit as Qubit_,
    Expression,
    MemoryReference,
    quil_sin,
    quil_cos,
    Add as Add_,
    Sub,
    Mul as Mul_,
    Div,
    Pow as Pow_,
    Function as Function_,
)
from pyquil.quilbase import Declare, Gate, Halt, Measurement, Pragma
from sympy import pi, Expr, Symbol, sin, cos, Number, Add, Mul, Pow

from pytket.circuit import Circuit, Node, OpType, Qubit, Bit
from pytket.architecture import Architecture

_known_quil_gate = {
    "X": OpType.X,
    "Y": OpType.Y,
    "Z": OpType.Z,
    "H": OpType.H,
    "S": OpType.S,
    "T": OpType.T,
    "RX": OpType.Rx,
    "RY": OpType.Ry,
    "RZ": OpType.Rz,
    "CZ": OpType.CZ,
    "CNOT": OpType.CX,
    "CCNOT": OpType.CCX,
    "CPHASE": OpType.CU1,
    "PHASE": OpType.U1,
    "SWAP": OpType.SWAP,
    "XY": OpType.ISWAP,
}


_known_quil_gate_rev = {v: k for k, v in _known_quil_gate.items()}


def param_to_pyquil(p: Union[float, Expr]) -> Union[float, Expression]:
    ppi = p * pi
    if len(ppi.free_symbols) == 0:
        return float(ppi.evalf())
    else:

        def to_pyquil(e: Expr) -> Union[float, Expression]:  # type: ignore
            if isinstance(e, Number):
                return float(e)
            elif isinstance(e, Symbol):
                return MemoryReference(str(e))
            elif isinstance(e, sin):
                return quil_sin(to_pyquil(e))
            elif isinstance(e, cos):
                return quil_cos(to_pyquil(e))
            elif isinstance(e, Add):
                args = [to_pyquil(a) for a in e.args]
                acc = args[0]
                for a in args[1:]:
                    acc += a
                return acc
            elif isinstance(e, Mul):
                args = [to_pyquil(a) for a in e.args]
                acc = args[0]
                for a in args[1:]:
                    acc *= a
                return acc
            elif isinstance(e, Pow):
                args = Pow_(to_pyquil(e.base), to_pyquil(e.exp))  # type: ignore
            elif e == pi:
                return math.pi
            else:
                raise NotImplementedError(
                    "Sympy expression could not be converted to a Quil expression: "
                    + str(e)
                )

        return to_pyquil(ppi)


def param_from_pyquil(p: Union[float, Expression]) -> Expr:
    def to_sympy(e: Any) -> Union[float, int, Expr, Symbol]:
        if isinstance(e, (float, int)):
            return e
        elif isinstance(e, MemoryReference):
            return Symbol(e.name)  # type: ignore
        elif isinstance(e, Function_):
            if e.name == "SIN":
                return sin(to_sympy(e.expression))  # type: ignore
            elif e.name == "COS":
                return cos(to_sympy(e.expression))  # type: ignore
            else:
                raise NotImplementedError(
                    "Quil expression function "
                    + e.name
                    + " cannot be converted to a sympy expression"
                )
        elif isinstance(e, Add_):
            return to_sympy(e.op1) + to_sympy(e.op2)
        elif isinstance(e, Sub):
            return to_sympy(e.op1) - to_sympy(e.op2)
        elif isinstance(e, Mul_):
            return to_sympy(e.op1) * to_sympy(e.op2)
        elif isinstance(e, Div):
            return to_sympy(e.op1) / to_sympy(e.op2)
        elif isinstance(e, Pow_):
            return to_sympy(e.op1) ** to_sympy(e.op2)
        else:
            raise NotImplementedError(
                "Quil expression could not be converted to a sympy expression: "
                + str(e)
            )

    return to_sympy(p) / pi  # type: ignore


[docs] def pyquil_to_tk(prog: Program) -> Circuit: """ Convert a :py:class:`pyquil.Program` to a tket :py:class:`Circuit` . Note that not all pyQuil operations are currently supported by pytket. :param prog: A circuit to be converted :return: The converted circuit """ tkc = Circuit() qmap = {} for q in prog.get_qubits(): uid = Qubit("q", q) # type: ignore tkc.add_qubit(uid) qmap.update({q: uid}) cregmap: Dict = {} for i in prog.instructions: if isinstance(i, Gate): try: optype = _known_quil_gate[i.name] except KeyError as error: raise NotImplementedError( "Operation not supported by tket: " + str(i) ) from error qubits = [qmap[q.index] for q in i.qubits] params: list[Union[Expr, float]] = [param_from_pyquil(p) for p in i.params] # type: ignore tkc.add_gate(optype, params, qubits) elif isinstance(i, Measurement): qubit = qmap[i.qubit.index] reg = cregmap[i.classical_reg.name] # type: ignore bit = reg[i.classical_reg.offset] # type: ignore tkc.Measure(qubit, bit) elif isinstance(i, Declare): if i.memory_type == "BIT": new_reg = tkc.add_c_register(i.name, i.memory_size) cregmap.update({i.name: new_reg}) elif i.memory_type == "REAL": continue else: raise NotImplementedError( "Cannot handle memory of type " + i.memory_type ) elif isinstance(i, Pragma): continue elif isinstance(i, Halt): return tkc else: raise NotImplementedError("PyQuil instruction is not a gate: " + str(i)) return tkc
@overload def tk_to_pyquil( tkcirc: Circuit, active_reset: bool = False, return_used_bits: Literal[False] = ... ) -> Program: ... @overload def tk_to_pyquil( tkcirc: Circuit, active_reset: bool = False, *, return_used_bits: Literal[True] ) -> Tuple[Program, List[Bit]]: ... @overload def tk_to_pyquil( tkcirc: Circuit, active_reset: bool, return_used_bits: Literal[True] ) -> Tuple[Program, List[Bit]]: ...
[docs] def tk_to_pyquil( tkcirc: Circuit, active_reset: bool = False, return_used_bits: bool = False ) -> Union[Program, Tuple[Program, List[Bit]]]: """ Convert a tket :py:class:`Circuit` to a :py:class:`pyquil.Program` . :param tkcirc: A circuit to be converted :return: The converted circuit """ p = Program() qregs = set() for qbt in tkcirc.qubits: if len(qbt.index) != 1: raise NotImplementedError("PyQuil registers must use a single index") qregs.add(qbt.reg_name) if len(qregs) > 1: raise NotImplementedError( "Cannot convert circuit with multiple quantum registers to pyQuil" ) creg_sizes: Dict = {} for b in tkcirc.bits: if len(b.index) != 1: raise NotImplementedError("PyQuil registers must use a single index") if (b.reg_name not in creg_sizes) or (b.index[0] >= creg_sizes[b.reg_name]): creg_sizes.update({b.reg_name: b.index[0] + 1}) cregmap = {} for reg_name, size in creg_sizes.items(): name = reg_name if name == "c": name = "ro" quil_reg = p.declare(name, "BIT", size) cregmap.update({reg_name: quil_reg}) for sym in tkcirc.free_symbols(): p.declare(str(sym), "REAL") if active_reset: p.reset() measures = [] measured_qubits: List[Qubit] = [] used_bits: List[Bit] = [] for command in tkcirc: op = command.op optype = op.type if optype == OpType.Measure: qbt = Qubit_(command.args[0].index[0]) # type: ignore if qbt in measured_qubits: raise NotImplementedError( "Cannot apply gate on qubit " + qbt.__repr__() + " after measurement" ) bit = cast(Bit, command.args[1]) b = cregmap[bit.reg_name][bit.index[0]] # type: ignore measures.append(Measurement(qbt, b)) # type: ignore measured_qubits.append(qbt) used_bits.append(bit) continue elif optype == OpType.Barrier: continue # pyQuil cannot handle barriers qubits = [Qubit_(qb.index[0]) for qb in command.args] for qbt in qubits: # type: ignore if qbt in measured_qubits: raise NotImplementedError( "Cannot apply gate on qubit " + qbt.__repr__() + " after measurement" ) try: gatetype = _known_quil_gate_rev[optype] except KeyError as error: raise NotImplementedError( "Cannot convert tket Op to pyQuil gate: " + op.get_name() ) from error params = [param_to_pyquil(p) for p in op.params] g = Gate(gatetype, params, qubits) p += g for m in measures: p += m if return_used_bits: return p, used_bits return p
def process_characterisation(qc: QuantumComputer) -> dict: """Convert a :py:class:`pyquil.api.QuantumComputer` to a dictionary containing Rigetti device Characteristics :param qc: A quantum computer to be converted :type qc: QuantumComputer :return: A dictionary containing Rigetti device characteristics """ isa = qc.quantum_processor.to_compiler_isa() coupling_map = [(int(e.ids[0]), int(e.ids[1])) for e in isa.edges.values()] str_to_gate_1qb = { "RX": { "PI": OpType.X, "PIHALF": OpType.V, "-PIHALF": OpType.Vdg, "-PI": OpType.X, "ANY": OpType.Rx, }, "RZ": { "ANY": OpType.Rz, }, } str_to_gate_2qb = {"CZ": OpType.CZ, "XY": OpType.ISWAP} link_errors: Dict[Tuple[Node, Node], Dict[OpType, float]] = defaultdict(dict) node_errors: Dict[Node, Dict[OpType, float]] = defaultdict(dict) readout_errors: dict = {} # T1s and T2s are currently left empty t1_times_dict: dict = {} t2_times_dict: dict = {} for q in isa.qubits.values(): node = Node(q.id) for g in q.gates: if g.fidelity is None: g.fidelity = 1.0 if isinstance(g, GateInfo) and g.operator in str_to_gate_1qb: angle = _get_angle_type(g.parameters[0]) if angle is not None: try: optype = str_to_gate_1qb[g.operator][angle] except KeyError: warning( f"Ignoring unrecognised angle {g.parameters[0]} " f"for gate {g.operator}. This may mean that some " "hardware-supported gates won't be used." ) continue if node in node_errors and optype in node_errors[node]: if abs(1.0 - g.fidelity - node_errors[node][optype]) > 1e-7: # fidelities for Rx(PI) and Rx(-PI) are given, hopefully # they are always identical warning( f"Found two differing fidelities for {optype} on node " f"{node}, using error = {node_errors[node][optype]}" ) else: node_errors[node].update({optype: 1.0 - g.fidelity}) elif isinstance(g, MeasureInfo) and g.operator == "MEASURE": # for some reason, there are typically two MEASURE entries, # one with target="_", and one with target=Node # in all pyquil code I have seen, both have the same value if node in readout_errors: if abs(1.0 - g.fidelity - readout_errors[node]) > 1e-7: warning( f"Found two differing readout fidelities for node {node}," f" using RO error = {readout_errors[node]}" ) else: readout_errors[node] = 1.0 - g.fidelity elif g.operator == "I": continue else: warning(f"Ignoring fidelity for unknown operator {g.operator}") for e in isa.edges.values(): n1, n2 = Node(e.ids[0]), Node(e.ids[1]) for g in e.gates: if g.fidelity is None: g.fidelity = 1.0 if g.operator in str_to_gate_2qb: optype = str_to_gate_2qb[g.operator] link_errors[(n1, n2)].update({optype: 1.0 - g.fidelity}) else: warning(f"Ignoring fidelity for unknown operator {g.operator}") arc = Architecture(coupling_map) characterisation = dict() characterisation["NodeErrors"] = node_errors characterisation["EdgeErrors"] = link_errors # type: ignore characterisation["Architecture"] = arc # type: ignore characterisation["t1times"] = t1_times_dict characterisation["t2times"] = t2_times_dict return characterisation def _get_angle_type(angle: Union[float, str]) -> Optional[str]: if angle == "theta": return "ANY" else: angles = {pi: "PI", pi / 2: "PIHALF", 0: None, -pi / 2: "-PIHALF", -pi: "-PI"} if not isinstance(angle, str): for val, code in angles.items(): if abs(angle - val) < 1e-7: return code warning( f"Ignoring unrecognised angle {angle}. This may mean that some " "hardware-supported gates won't be used." ) return None 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"] ) avg: Callable[[Dict[Any, float]], float] = lambda xs: sum(xs.values()) / len(xs) avg_node_errors = map_values(avg, node_errors) avg_link_errors = map_values(avg, link_errors) return { "node_errors": avg_node_errors, "link_errors": avg_link_errors, }