Source code for pytket.extensions.projectq.projectq_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 ProjectQ and tket data types
"""

from typing import Any, Iterable

import numpy as np

from projectq import ops as pqo  # type: ignore
from projectq.cengines import BasicEngine, LastEngineException  # type: ignore
from projectq import MainEngine
from projectq.meta import get_control_count  # type: ignore
from projectq.ops._command import Command as ProjectQCommand, apply_command  # type: ignore
from projectq.types._qubit import Qureg  # type: ignore
from pytket.circuit import OpType, Op, Circuit, Command, Bit
from pytket.transform import Transform
from pytket.passes import RebaseCustom
from pytket.circuit_library import CX, TK1_to_RzRx

_pq_to_tk_singleqs = {
    pqo.XGate: OpType.X,
    pqo.YGate: OpType.Y,
    pqo.ZGate: OpType.Z,
    pqo.Rx: OpType.Rx,
    pqo.Ry: OpType.Ry,
    pqo.Rz: OpType.Rz,
    pqo.HGate: OpType.H,
    pqo.SGate: OpType.S,
    pqo.TGate: OpType.T,
    pqo.SqrtXGate: OpType.V,
    pqo.MeasureGate: OpType.Measure,
}

# python can't hash projectq controlled gates...
_pq_to_tk_multiqs = {pqo.XGate: OpType.CX, pqo.ZGate: OpType.CZ, pqo.Rz: OpType.CRz}

# Other gates will be added here which are neither controlled operations nor valid tket
# Ops. These gates are currently either ignored (Barrier) or used to determine flushing
# of tketOptimiser (FlushGate)
_OTHER_KNOWN_GATES = {
    pqo.Allocate: OpType.noop,
    pqo.Deallocate: OpType.noop,
    pqo.Barrier: OpType.noop,
    pqo.FlushGate: OpType.noop,
    pqo.SwapGate: OpType.SWAP,
}


_ALLOWED_GATES = {**_pq_to_tk_singleqs, **_pq_to_tk_multiqs, **_OTHER_KNOWN_GATES}
_REBASE = RebaseCustom(
    {
        OpType.SWAP,
        OpType.CRz,
        OpType.CX,
        OpType.CZ,
        OpType.H,
        OpType.X,
        OpType.Y,
        OpType.Z,
        OpType.S,
        OpType.T,
        OpType.V,
        OpType.Rx,
        OpType.Ry,
        OpType.Rz,
    },
    CX(),
    TK1_to_RzRx,
)
_tk_to_pq_singleqs: dict = dict(
    ((item[1], item[0]) for item in _pq_to_tk_singleqs.items())
)
_tk_to_pq_multiqs: dict = dict(
    ((item[1], item[0]) for item in _pq_to_tk_multiqs.items())
)


def _get_pq_command_from_tk_command(
    command: Command, engine: MainEngine, container: Any
) -> ProjectQCommand:
    op = command.op
    optype = op.type
    controlled = False
    if optype in _tk_to_pq_singleqs:
        gatetype = _tk_to_pq_singleqs[optype]
    elif optype in _tk_to_pq_multiqs:
        gatetype = _tk_to_pq_multiqs[optype]
        controlled = True
    else:
        raise Exception("Cannot convert op " + str(command) + " to projectq")

    if issubclass(gatetype, pqo.BasicRotationGate):
        params = op.params
        if len(params) != 1:
            raise Exception(f"A Rotation Gate has {len(params)} parameters")
        try:
            gate = gatetype(params[0].evalf() * np.pi)  # type: ignore
        except:
            gate = gatetype(params[0] * np.pi)
    elif issubclass(gatetype, pqo.BasicGate):
        gate = gatetype()
    else:
        raise Exception("Gate of type: " + str(gatetype) + " cannot be converted")
    qubs = [q.index[0] for q in command.args]
    if controlled:
        target = container[qubs[-1]]
        qubs.pop()
        controls = (container[i] for i in qubs)
        qubits = gate.make_tuple_of_qureg(target)
        cmd = ProjectQCommand(engine, gate, qubits, controls)
    else:
        qubits = gate.make_tuple_of_qureg(container[i] for i in qubs)
        cmd = ProjectQCommand(engine, gate, qubits)

    return cmd


[docs] def tk_to_projectq( engine: MainEngine, qureg: Qureg, circuit: Circuit, ignore_measures: bool = False ) -> None: """Given a ProjectQ Qureg in an Engine, converts a Circuit to a series of ProjectQ Commands on this Qureg. :param engine: A ProjectQ MainEngine :type engine: MainEngine :param qureg: A ProjectQ Qureg in this MainEngine :type qureg: Qureg :param circuit: A tket Circuit :type circuit: Circuit """ if not circuit.is_simple: raise Exception("Cannot currently convert non-simple circuits to ProjectQ") for command in circuit: if ignore_measures and command.op.type == OpType.Measure: continue if command.op.type == OpType.Barrier: continue cmd = _get_pq_command_from_tk_command(command, engine, qureg) apply_command(cmd)
def _handle_gate( command: ProjectQCommand, engine: Any ) -> None: # must also be a tket Engine if command.gate in _OTHER_KNOWN_GATES or type(command.gate) in _OTHER_KNOWN_GATES: return elif ( type(command.gate) in _pq_to_tk_multiqs and len(command.control_qubits) > 0 and len(command.qubits) > 0 ): engine._translate_multi_qubit_op(command) elif ( type(command.gate) in _pq_to_tk_singleqs and len(command.control_qubits) == 0 and len(command.qubits) == 1 ): engine._translate_single_qubit_op(command) elif type(command.gate) == pqo.DaggeredGate: engine._translate_daggered_op(command) else: raise Exception( "uncaught option " + str(command.gate) + " controls = " + str(len(command.control_qubits)) + " targets = " + str(len(command.qubits)) ) def _add_daggered_op_to_circuit(cmd: ProjectQCommand, circ: Circuit) -> bool: undaggered_gate = cmd.gate.get_inverse() if type(undaggered_gate) == pqo.TGate: op = Op.create(OpType.Tdg) elif type(undaggered_gate) == pqo.SGate: op = Op.create(OpType.Sdg) else: raise Exception("cannot recognise daggered op of type " + str(cmd.gate)) qubit_no = cmd.qubits[0][0].id assert len(cmd.qubits) == 1 assert len(cmd.qubits[0]) == 1 new_qubit = False if qubit_no >= circ.n_qubits: circ.add_blank_wires(1 + qubit_no - circ.n_qubits) new_qubit = True circ.add_gate(Op=op, args=[qubit_no]) return new_qubit def _add_single_qubit_op_to_circuit(cmd: ProjectQCommand, circ: Circuit) -> bool: assert len(cmd.qubits) == 1 assert len(cmd.qubits[0]) == 1 qubit_no = cmd.qubits[0][0].id new_qubit = False if get_control_count(cmd) > 0: raise Exception( "singleq gate " + str(cmd.gate) + " has " + str(get_control_count(cmd)) + " control qubits" ) else: if qubit_no >= circ.n_qubits: circ.add_blank_wires(1 + qubit_no - circ.n_qubits) new_qubit = True if type(cmd.gate) == pqo.MeasureGate: bit = Bit("c", qubit_no) if bit not in circ.bits: circ.add_bit(bit) circ.Measure(qubit_no, qubit_no) return new_qubit elif type(cmd.gate) in (pqo.Rx, pqo.Ry, pqo.Rz): op = Op.create(_pq_to_tk_singleqs[type(cmd.gate)], cmd.gate.angle / np.pi) else: op = Op.create(_pq_to_tk_singleqs[type(cmd.gate)]) circ.add_gate(Op=op, args=[qubit_no]) return new_qubit def _add_multi_qubit_op_to_circuit(cmd: ProjectQCommand, circ: Circuit) -> list: assert len(cmd.qubits) > 0 qubs = [qb for qr in cmd.all_qubits for qb in qr] if get_control_count(cmd) < 1: raise Exception("multiq gate " + str(cmd.gate) + " has no controls") else: new_qubits = [] for q in qubs: qubit_no = q.id if qubit_no >= circ.n_qubits: circ.add_blank_wires(1 + qubit_no - circ.n_qubits) new_qubits.append(q) if type(cmd.gate) == pqo.CRz: op = Op.create(_pq_to_tk_multiqs[type(cmd.gate)], cmd.gate.angle / np.pi) else: op = Op.create(_pq_to_tk_multiqs[type(cmd.gate)]) qubit_nos = [qb.id for qr in cmd.all_qubits for qb in qr] circ.add_gate(Op=op, args=qubit_nos) return new_qubits
[docs] class tketBackendEngine(BasicEngine): """ A projectq backend designed to translate from projectq commands to tket Circuits """ def __init__(self) -> None: """ Initialize the tketBackendEngine. Initializes local Circuit to an empty Circuit. """ BasicEngine.__init__(self) self._circuit = Circuit() @property def circuit(self) -> Circuit: """ :raises NotImplementedError: If the Circuit has no gates, assumes user forgot to flush engines. :return: The tket Circuit from the engine. """ if self._circuit.n_gates == 0: raise Exception("Circuit has no gates. Have you flushed your engine?") return self._circuit
[docs] def is_available(self, cmd: ProjectQCommand) -> bool: """ Ask the next engine whether a command is available, i.e., whether it can be executed by the next engine(s). :raises LastEngineException: If is_last_engine is True but is_available is not implemented. :param cmd: Command for which to check availability. :return: True if the command can be executed. """ try: return bool(BasicEngine.is_available(self, cmd)) except LastEngineException: return True
[docs] def receive(self, command_list: Iterable) -> None: """Process commands from a list and append to local Circuit.""" for cmd in command_list: _handle_gate(cmd, self)
def _translate_daggered_op(self, cmd: ProjectQCommand) -> None: # assume it is a single qubit op _add_daggered_op_to_circuit(cmd, self._circuit) def _translate_single_qubit_op(self, cmd: ProjectQCommand) -> None: _add_single_qubit_op_to_circuit(cmd, self._circuit) def _translate_multi_qubit_op(self, cmd: ProjectQCommand) -> None: _add_multi_qubit_op_to_circuit(cmd, self._circuit)
[docs] class tketOptimiser(BasicEngine): """ A ProjectQ BasicEngine designed to translate from ProjectQ commands to tket Circuits, optimise them, and then return other ProjectQ commands. """ def __init__(self) -> None: BasicEngine.__init__(self) self._circuit = Circuit() self._qubit_dictionary: dict = dict()
[docs] def receive(self, command_list: list) -> None: """ Receives a list of commands and appends to local Circuit. If a flush gate is received, optimises the Circuit using a default Transform pass and then sends the commands from this optimised Circuit into the next engine. """ for cmd in command_list: if cmd.gate == pqo.FlushGate(): # flush gate --> optimize and then flush cmd_list = self._optimise() cmd_list.append(cmd) self._circuit = Circuit() self._qubit_dictionary = dict() self.send(cmd_list) continue _handle_gate(cmd, self)
def _optimise( self, ) -> list: # takes the circuit and optimises it before regurgitating it as a series of # ProjectQ commands if self._circuit.n_qubits != 0: Transform.OptimisePhaseGadgets().apply(self._circuit) _REBASE.apply(self._circuit) cmd_list = [] for i in range(self._circuit.n_qubits): gate = pqo.Allocate cmd = ProjectQCommand( self.main_engine, gate, gate.make_tuple_of_qureg(self._qubit_dictionary[i]), ) cmd_list.append(cmd) if self._circuit.n_gates == 0: return cmd_list for command in self._circuit: cmd = _get_pq_command_from_tk_command( command, self.main_engine, self._qubit_dictionary ) cmd_list.append(cmd) return cmd_list def _translate_daggered_op(self, cmd: ProjectQCommand) -> None: # assume it is a single qubit op, as the only daggered ops which are of the # ProjectQ DaggeredGate class are single qubit new_qubit = _add_daggered_op_to_circuit(cmd, self._circuit) # if this qubit hasn't been seen before by the circuit, add to dictionary if new_qubit: self._qubit_dictionary[cmd.qubits[0][0].id] = cmd.qubits[0][0] def _translate_single_qubit_op(self, cmd: ProjectQCommand) -> None: new_qubit = _add_single_qubit_op_to_circuit(cmd, self._circuit) if new_qubit: self._qubit_dictionary[cmd.qubits[0][0].id] = cmd.qubits[0][0] def _translate_multi_qubit_op(self, cmd: ProjectQCommand) -> None: new_qubits = _add_multi_qubit_op_to_circuit(cmd, self._circuit) for q in new_qubits: self._qubit_dictionary[q.id] = q