Source code for qrisp.circuit.quantum_circuit

"""
\********************************************************************************
* Copyright (c) 2023 the Qrisp authors
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is
* available at https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
"""


import numpy as np
import sympy

import qrisp.circuit.standard_operations as ops
from qrisp.circuit import Clbit, Instruction, Operation, Qubit

# Class to describe quantum circuits
# The naming of the attributes is rather similar to the qiskit equivalent
# in order to allow compatibility of qiskit programs to qrisp
# The key attributes are

# The list of qubits (.qubits).
# the list of classical bits (.clbits)
# the list of instructions (.data)


[docs] class QuantumCircuit: """ This class describes quantum circuits. Many of the attribute and method names are oriented at the `Qiskit QuantumCircuit <https://qiskit.org/documentation/stubs/qiskit.circuit.QuantumCircuit.html>`_ class in order to provide a high degree of compatibility. QuantumCircuits can be visualized by calling ``print`` on them. Qrisp QuantumCircuits can be quickly generated out of existing Qiskit QuantumCircuits with the :meth:`from_qiskit <qrisp.QuantumCircuit.from_qiskit>` method. Parameters ---------- num_qubits : integer, optional The amount of qubits, this QuantumCircuit is initialized with. The default is 0. num_clbits : integer, optional The amount of classical bits. The default is 0. name : string, optional A name for the QuantumCircuit. The default will generated a generic name. Examples -------- We create a QuantumCircuit containing a so-called fan-out gate: >>> from qrisp import QuantumCircuit >>> qc_0 = QuantumCircuit(4, name = "fan out") >>> qc_0.cx(0, range(1,4)) >>> print(qc_0) :: qb_0: ──■────■────■── ┌─┴─┐ │ │ qb_1: ┤ X ├──┼────┼── └───┘┌─┴─┐ │ qb_2: ─────┤ X ├──┼── └───┘┌─┴─┐ qb_3: ──────────┤ X ├ └───┘ Note that the :meth:`cx gate appending method <qrisp.QuantumCircuit.cx>` (like all other gate appending methods) can be called with integers, Qubit objects, lists of integers or lists of Qubit objects. We now turn this QuantumCircuit into a gate and append to another QuantumCircuit to generate a GHZ state: >>> qc_1 = QuantumCircuit(4) >>> qc_1.h(0) >>> qc_1.append(qc_0.to_gate(), qc_1.qubits) >>> print(qc_1) :: ┌───┐┌──────────┐ qb_4: ┤ H ├┤0 ├ └───┘│ │ qb_5: ─────┤1 ├ │ fan out │ qb_6: ─────┤2 ├ │ │ qb_7: ─────┤3 ├ └──────────┘ Finally, we add a measurement and evaluate the circuit: >>> qc_1.measure(qc_1.qubits) >>> print(qc_1) :: ┌───┐┌──────────┐┌─┐ qb_4: ┤ H ├┤0 ├┤M├───────── └───┘│ │└╥┘┌─┐ qb_5: ─────┤1 ├─╫─┤M├────── │ fan out │ ║ └╥┘┌─┐ qb_6: ─────┤2 ├─╫──╫─┤M├─── │ │ ║ ║ └╥┘┌─┐ qb_7: ─────┤3 ├─╫──╫──╫─┤M├ └──────────┘ ║ ║ ║ └╥┘ cb_0: ══════════════════╩══╬══╬══╬═ ║ ║ ║ cb_1: ═════════════════════╩══╬══╬═ ║ ║ cb_2: ════════════════════════╩══╬═ cb_3: ═══════════════════════════╩═ >>> qc_1.run(shots = 1000) {'0000': 500, '1111': 500} **Converting from Qiskit** We construct the very same fan out QuantumCircuit in Qiskit: >>> from qiskit import QuantumCircuit as QiskitQuantumCircuit >>> qc_2 = QiskitQuantumCircuit(4) >>> qc_2.cx(0, range(1,4)) >>> print(qc_2) :: q_0: ──■────■────■── ┌─┴─┐ │ │ q_1: ┤ X ├──┼────┼── └───┘┌─┴─┐ │ q_2: ─────┤ X ├──┼── └───┘┌─┴─┐ q_3: ──────────┤ X ├ └───┘ To acquire the Qrisp QuantumCircuit we call the :meth:`from_qiskit <qrisp.QuantumCircuit.from_qiskit>` method. Note that we don't need to create a QuantumCircuit object first as this is a class method. >>> qrisp_qc_2 = QuantumCircuit.from_qiskit(qc_2) >>> print(qrisp_qc_2) :: qb_8: ──■────■────■── ┌─┴─┐ │ │ qb_9: ┤ X ├──┼────┼── └───┘┌─┴─┐ │ qb_10: ─────┤ X ├──┼── └───┘┌─┴─┐ qb_11: ──────────┤ X ├ └───┘ **Abstract Parameters** Abstract parameters are represented by `Sympy symbols <https://docs.sympy.org/latest/modules/core.html#module-sympy.core.symbol>`_ in Qrisp. We create a QuantumCircuit with some abstract parameters and bind them subsequently. >>> from qrisp import QuantumCircuit >>> from sympy import symbols >>> qc = QuantumCircuit(3) Create some Sympy symbols and use them as abstract parameters for phase gates: >>> abstract_parameters = symbols("a b c") >>> for i in range(3): qc.p(abstract_parameters[i], i) Create the substitution dictionary and bind the parameters: >>> subs_dic = {abstract_parameters[i] : i for i in range(3)} >>> bound_qc = qc.bind_parameters(subs_dic) >>> print(bound_qc) :: ┌──────┐ qb_0: ┤ P(0) ├ ├──────┤ qb_1: ┤ P(1) ├ ├──────┤ qb_2: ┤ P(2) ├ └──────┘ """ qubit_index_counter = np.zeros(1, dtype=int) xla_mode = 0 def __init__(self, num_qubits=0, num_clbits=0, name=None): object.__setattr__(self, "data", []) object.__setattr__(self, "qubits", []) object.__setattr__(self, "clbits", []) self.abstract_params = set() if isinstance(num_qubits, int): for i in range(num_qubits): self.qubits.append(Qubit("qb_" + str(self.qubit_index_counter[0]+i))) self.qubit_index_counter[0] += num_qubits else: raise Exception( f"Tried to initialize QuantumCircuit with type {type(num_qubits)}" ) if isinstance(num_clbits, int): for i in range(num_clbits): self.add_clbit() else: raise Exception( f"Tried to initialize QuantumCircuit with type {type(num_clbits)}" ) # Method to add qubit objects to the circuit
[docs] def add_qubit(self, qubit=None): """ Adds a Qubit to the QuantumCircuit. Parameters ---------- qubit : Qubit, optional The Qubit to be added. If given none, a new Qubit will be generated. Returns ------- Qubit The added Qubit. Examples -------- >>> from qrisp import QuantumCircuit >>> qc = QuantumCircuit() >>> qc.add_qubit() >>> qc.qubits [qb_0] """ self.qubit_index_counter += 1 if qubit is None: qubit = Qubit("qb_" + str(self.qubit_index_counter[0])) if self.xla_mode < 2: for qb in self.qubits: if qb.identifier == qubit.identifier: raise Exception(f"Qubit name {qubit.identifier} already exists") if not isinstance(qubit, Qubit): raise Exception(f"Tried to add type {type(qubit)} as a qubit") self.qubits.append(qubit) return self.qubits[-1]
# Method to add classical bit objects to the circuit
[docs] def add_clbit(self, clbit=None): """ Adds a classical bit to the QuantumCircuit. Parameters ---------- clbit : Clbit, optional The classical bit to be added. If given none, a new Clbit will be generated. Returns ------- Clbit The added Clbit. """ if clbit is None: clbit = Clbit("cb_" + str(len(self.clbits))) if not isinstance(clbit, Clbit): raise Exception(f"Tried to add type {type(clbit)} as a classical bit") for cb in self.clbits: if cb.identifier == clbit.identifier: raise Exception(f"Clbit name {clbit.identifier} already exists") self.clbits.append(clbit) return self.clbits[-1]
# Method to transform the given circuit into an operation object
[docs] def to_op(self, name=None): """ Method to return an Operation object generated out of this QuantumCircuit. Operation objects can be appended to other QuantumCircuits. An alias for Qiskit compatibility is the :meth:`to_gate<qrisp.QuantumCircuit.to_gate>` method. Parameters ---------- name : string, optional The name of the gate. By default, the QuantumCircuit's name will be used. Returns ------- Operation The Operation defined by this QuantumCircuit. Examples -------- >>> from qrisp import QuantumCircuit >>> qc_0 = QuantumCircuit(4) >>> qc_0.x(qc.qubits) >>> operation = qc_0.to_gate() >>> qc_1 = QuantumCircuit(4) >>> qc_1.append(operation, qc_1.qubits) """ if name is None: name = "circuit" + str(id(self))[:5] definition = self.copy() i = 0 while i < len(definition.data): if definition.data[i].op.name in ["qb_alloc", "qb_dealloc"]: definition.data.pop(i) continue i += 1 return Operation( name=name, num_qubits=len(self.qubits), num_clbits=len(self.clbits), definition=definition, params=[], )
# Wrapper to increase Qiskit compatibility
[docs] def to_gate(self, name=None): """ Similar to :meth:`to_op <qrisp.QuantumCircuit.to_op>` but raises an exception if self contains classical bits (like the `Qiskit equivalent <https://qiskit.org/documentation/stubs/qiskit.circuit.QuantumCircuit.to_gate.html>`_). # noqa Parameters ---------- name : str, optional A name for the resulting gate. The default is None. Raises ------ Exception Tried to turn a circuit including classical bits into unitary gate Returns ------- Operation The QuantumCircuit turned into an :ref:`Operation` instance. """ if len(self.clbits) != 0: raise Exception( "Tried to turn a circuit including classical bits into unitary gate" ) return self.to_op(name)
# Method to extend the given circuit with another circuit # The dic translation dic encodes how the qubits should be plugged into each other
[docs] def extend(self, other, translation_dic="id"): """ Extends self in-place by another QuantumCircuit. Parameters ---------- other : QuantumCircuit The QuantumCircuit to extend by. translation_dic : dict, optional The dictionary containing the information about which Qubits and Clbits should be plugged into each other. This dictionary should contain qubits of other as keys and qubits of self as values. If given none, it is assumed that both QuantumCircuits have matching Qubits. Examples -------- We create two QuantumCircuits and extend the first with reversed qubit order by the other: >>> from qrisp import QuantumCircuit >>> extension_qc = QuantumCircuit(4) >>> qc_to_extend = QuantumCircuit(4) >>> extension_qc.cx(0, 1) >>> extension_qc.cy(0, 2) >>> extension_qc.cz(0, 3) >>> print(extension_qc) :: qb_0: ──■────■────■── ┌─┴─┐ │ │ qb_1: ┤ X ├──┼────┼── └───┘┌─┴─┐ │ qb_2: ─────┤ Y ├──┼── └───┘┌─┴─┐ qb_3: ──────────┤ Z ├ └───┘ >>> translation_dic = {extension_qc.qubits[i] : qc_to_extend.qubits[-1-i] >>> for i in range(4)} >>> qc_to_extend.extend(extension_qc, translation_dic) >>> print(qc_to_extend) :: ┌───┐ qb_4: ──────────┤ Z ├ ┌───┐└─┬─┘ qb_5: ─────┤ Y ├──┼── ┌───┐└─┬─┘ │ qb_6: ┤ X ├──┼────┼── └─┬─┘ │ │ qb_7: ──■────■────■── """ if translation_dic == "id": translation_dic = {} for qb in other.qubits: translation_dic[qb] = qb for cb in other.clbits: translation_dic[cb] = cb # Copy in order to prevent modification translation_dic = dict(translation_dic) for key in list(translation_dic.keys()): if isinstance(key, (Qubit, Clbit)): translation_dic[key.identifier] = translation_dic[key] for i in range(len(other.data)): instruction_other = other.data[i] qubits = [] for qb in instruction_other.qubits: qubits.append(translation_dic[qb.identifier]) clbits = [] for cb in instruction_other.clbits: clbits.append(translation_dic[cb.identifier]) self.append(instruction_other.op, qubits, clbits)
# Returns a copy of self
[docs] def copy(self): """ Returns a copy of the given QuantumCircuit. Returns ------- QuantumCircuit The copied QuantumCircuit. """ # If an inital circuit is given we construct a new instance res = QuantumCircuit() object.__setattr__(res, "data", list(self.data)) object.__setattr__(res, "qubits", list(self.qubits)) object.__setattr__(res, "clbits", list(self.clbits)) try: res.abstract_params = set(self.abstract_params) except AttributeError: pass return res
# Returns a copy of self but with no instructions
[docs] def clearcopy(self): """ Returns a copy of the given QuantumCircuit but without any data (i.e. just the Qubits and Clbits). Returns ------- QuantumCircuit The empty, copied QuantumCircuit. """ temp_data = list(self.data) self.data = [] res = self.copy() self.data = temp_data return res
# TO-DO write qiskit independent printer # Printing method def __str__(self): from qiskit.visualization.circuit_visualization import circuit_drawer from qrisp.interface import convert_to_qiskit try: res_str = str( circuit_drawer( convert_to_qiskit(self, transpile=False), output="text", cregbundle=False, ) ) except AttributeError: raise Exception( "Tried to print QuantumSession with uncompiled QuantumEnvironments" ) return res_str # Method which compares the unitary of two given circuits and returns # True if they are equivalent
[docs] def compare_unitary(self, other, precision=4, ignore_gphase=False): """ Compares the unitaries of two QuantumCircuits. This can be used to check if a QuantumCircuit transformation is valid. Parameters ---------- other : QuantumCircuit The QuantumCircuit to compare to. precision : int, optional The precision of the comparison. This function will return True, if the norm of the difference of the unitaries is below the precision. The default is 4. ignore_gphase: bool, optional If set to True, this method returns True if the unitaries only differ in a global phase. Returns ------- Bool The comparison outcome. Examples -------- We create two QuantumCircuit with equivalent unitaries but differing by a non-trivial commutation: >>> from qrisp import QuantumCircuit >>> qc_0 = QuantumCircuit(2) >>> qc_1 = QuantumCircuit(2) >>> qc_0.z(0) >>> qc_0.cx(0,1) >>> print(qc_0) :: ┌───┐ qb_0: ┤ Z ├──■── └───┘┌─┴─┐ qb_1: ─────┤ X ├ └───┘ >>> qc_1.cx(0,1) >>> qc_1.z(0) >>> print(qc_1) :: ┌───┐ qb_2: ──■──┤ Z ├ ┌─┴─┐└───┘ qb_3: ┤ X ├───── └───┘ >>> qc_0.compare_unitary(qc_1) True """ if len(self.qubits) != len(other.qubits): return False unitary_self = self.get_unitary() unitary_other = other.get_unitary() if ignore_gphase: arg_max = np.argmax(np.abs(unitary_self.flatten())) unitary_self = ( unitary_self * unitary_other.flatten()[arg_max] / unitary_self.flatten()[arg_max] ) from numpy.linalg import norm return bool(norm(unitary_self - unitary_other) < 10**-precision)
# Converts several types of inputs to qubit lists. # Possible inputs are # # A qubit object # An integer # A list of integers # A list of qubits def convert_to_qubit_list(self, input, inner_recursion=False): if isinstance(input, Qubit): if inner_recursion: return input else: return [input] if isinstance(input, int): try: return self.convert_to_qubit_list( self.qubits[input], inner_recursion=inner_recursion ) except IndexError: raise Exception( "Not enough qubits in circuit to access qubit " + str(input) + "." ) if isinstance(input, list): return_list = [] for qb in input: return_list.append(self.convert_to_qubit_list(qb, inner_recursion=True)) return return_list raise Exception( "Could not convert input type " + type(input) + " to qubit list" ) # Similar function as above but with classical bits def convert_to_clbit_list(self, input, inner_recursion=False): if isinstance(input, Clbit): if inner_recursion: return input else: return [input] if isinstance(input, int): try: return self.convert_to_clbit_list( self.clbits[input], inner_recursion=inner_recursion ) except IndexError: raise Exception( "Not enough clbits in circuit to access clbit " + str(input) + "." ) if isinstance(input, list): return_list = [] for cb in input: return_list.append(self.convert_to_clbit_list(cb, inner_recursion=True)) return return_list return self.convert_to_clbit_list(list(input)) # Generates the inverse of self by applying the inverse gates in reversed order
[docs] def inverse(self): """ Returns the inverse/daggered QuantumCircuit. Returns ------- inverted_circuit : QuantumCircuit The inverted QuantumCircuit. Examples -------- Daggering a QuantumCircuit reverses the order and daggers each operation. >>> from qrisp import QuantumCircuit >>> import numpy as np >>> qc = QuantumCircuit(1) >>> qc.x(0) >>> qc.p(np.pi/2, 0) >>> qc.y(0) >>> print(qc.inverse()) :: ┌───┐┌─────────┐┌───┐ qb_0: ┤ Y ├┤ P(-π/2) ├┤ X ├ └───┘└─────────┘└───┘ For the phase gate, a daggering implies the reversal of the phase - Pauli gates however are invariant under daggering. """ inverted_circuit = self.clearcopy() for instr in self.data[::-1]: inverted_circuit.append(instr.op.inverse(), instr.qubits, instr.clbits) return inverted_circuit
# Generate the circuits unitary matrix
[docs] def get_unitary(self, decimals=-1): """ Acquires the unitary matrix of the given QuantumCircuit as a Numpy array. This method also works with abstract parameters. In this case a Numpy array with Sympy entries is returned. Parameters ---------- decimals : integer, optional The amount of decimals to be rounded to. By default, the full precision is returned. Returns ------- numpy.ndarray The unitary matrix as a numpy array. Examples -------- We synthesize a controlled phase gate and inspect the unitary: >>> from qrisp import QuantumCircuit >>> import numpy as np >>> qc = QuantumCircuit(2) >>> phi = np.pi >>> qc.p(phi/2, 0) >>> qc.p(phi/2, 1) >>> qc.cx(0,1) >>> qc.p(-phi/2, 1) >>> qc.cx(0,1) >>> qc.get_unitary(decimals = 4) array([[ 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [ 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], [ 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j], [ 0.+0.j, 0.+0.j, 0.+0.j, -1.+0.j]], dtype=complex64) We now synthesize the exact same QuantumCircuit but this time ``phi`` is a Sympy symbol. >>> from sympy import Symbol >>> qc = QuantumCircuit(2) >>> phi = Symbol("phi") >>> qc.p(phi/2, 0) >>> qc.p(phi/2, 1) >>> qc.cx(0,1) >>> qc.p(-phi/2, 1) >>> qc.cx(0,1) >>> qc.get_unitary(decimals = 4) array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, exp(I*phi)]], dtype=object) """ from qrisp.simulator import calc_circuit_unitary res = calc_circuit_unitary(self) if decimals != -1: if res.dtype == np.dtype("O"): raveled_res = res.ravel() for i in range(len(raveled_res)): expression = sympy.simplify(raveled_res[i]) for a in sympy.preorder_traversal(expression): if isinstance(a, sympy.Float): rounded_float = round(a, decimals) if abs(float(a) - 1) < 10**-(decimals): expression = expression.subs(a, 1) else: expression = expression.subs(a, rounded_float) raveled_res[i] = expression else: res = np.round(res, decimals) return res
def get_depth_dic(self): from qrisp.misc import get_depth_dic return get_depth_dic(self) def cnot_count(self): """ Method to determine the amount of CNOT gates used in this QuantumCircuit. Returns ------- int The amount of CNOT gates. """ from qrisp.misc import cnot_count return cnot_count(self)
[docs] def transpile(self, transpilation_level=np.inf, **qiskit_kwargs): """ Transpiles the QuantumCircuit in the sense that there are no longer any synthesized gate objects. Furthermore, we can call the `Qiskit transpiler <https://qiskit.org/documentation/stubs/qiskit.compiler.transpile.html>`__ by supplying keyword arguments. The Qiskit transpiler is not called, if no keyword arguments are given. Parameters ---------- **qiskit_kwargs : Keyword arguments for the Qiskit transpiler. Returns ------- QuantumCircuit The transpiled QuantumCircuit. Examples -------- We create a QuantumCircuit and append a synthesized gate. Afterwards we transpile to a given set of basis gates using the Qiskit transpiler: >>> from qrisp import QuantumCircuit >>> qc = QuantumCircuit(3) >>> qc.mcx([0,1], 2) >>> print(qc) :: qb_0: ──■── qb_1: ──■── ┌─┴─┐ qb_2: ┤ X ├ └───┘ >>> print(qc.transpile(basis_gates = ["cx", "rz", "sx"])) :: global phase: 9π/8 ┌──────────┐ ┌───┐┌─────────┐ ┌───┐ ┌──────────┐┌───┐» qb_1: ┤ Rz(-π/4) ├─────────────────┤ X ├┤ Rz(π/4) ├───┤ X ├───┤ Rz(-π/4) ├┤ X ├» ├──────────┤ └─┬─┘└─────────┘ └─┬─┘ └──────────┘└─┬─┘» qb_2: ┤ Rz(-π/4) ├───────────────────┼───────■──────────■──────────■────────┼──» ├─────────┬┘┌────┐┌─────────┐ │ ┌─┴─┐ ┌─────────┐ ┌─┴─┐ │ » qb_3: ┤ Rz(π/2) ├─┤ √X ├┤ Rz(π/2) ├──■─────┤ X ├───┤ Rz(π/4) ├───┤ X ├──────■──» └─────────┘ └────┘└─────────┘ └───┘ └─────────┘ └───┘ » « ┌─────────┐┌───┐ «qb_1: ┤ Rz(π/4) ├┤ X ├──────────── « └─────────┘└─┬─┘ «qb_2: ─────────────■────────────── « ┌─────────┐┌────┐┌─────────┐ «qb_3: ┤ Rz(π/4) ├┤ √X ├┤ Rz(π/2) ├ « └─────────┘└────┘└─────────┘ """ from qrisp.circuit import transpile return transpile(self, transpilation_level, **qiskit_kwargs)
# Counts the amount of operations self contains and returns # a dict {"operatio_name" : operation_count, ...}
[docs] def count_ops(self): """ Counts the amount of operations of each kind. Note that operations are identified by their name. Returns ------- count_dic : dict A dictionary containing the gate counts. Examples -------- We create a QuantumCircuit containing a number of gates and evaluates the gate-counts: >>> from qrisp import QuantumCircuit >>> qc = QuantumCircuit(5) >>> qc.x(qc.qubits) >>> qc.cx(0, range(1,5)) >>> qc.z(1) >>> qc.t(range(5)) >>> qc.count_ops() {'x': 5, 'cx': 4, 'z': 1, 't': 5} """ count_dic = {} for ins in self.data: if ins.op.name in ["qb_alloc", "qb_dealloc"]: continue try: count_dic[ins.op.name] += 1 except KeyError: count_dic[ins.op.name] = 1 return count_dic
def control(self, amount): return self.to_gate().control(amount) def compose(self, other, qubits=[], clbits=[], inplace=True): if inplace: self.append(other.to_gate(), qubits, clbits) else: res = self.copy() res.append(other.to_gate(), qubits, clbits) return res
[docs] def bind_parameters(self, subs_dic): """ Returns a QuantumCircuit where the abstract parameters in ``subs_dic`` are bound to their specified values. Parameters ---------- subs_dic : dict A dictionary containing the abstract parameters of this QuantumCircuit as keys and the desired parameters as values. Raises ------ Exception ``subs_dic`` did not specify a value for all abstract parameters. Returns ------- QuantumCircuit The QuantumCircuit with substituted parameters. Examples -------- We create a QuantumCircuit with some abstract parameters and bind them subsequently: >>> from qrisp import QuantumCircuit >>> from sympy import symbols >>> qc = QuantumCircuit(3) Create some sympy symbols and use them as abstract parameters for phase gates: >>> abstract_parameters = symbols("a b c") >>> for i in range(3): qc.p(abstract_parameters[i], i) Create the substitution dictionary and bind the parameters: >>> subs_dic = {abstract_parameters[i] : i for i in range(3)} >>> bound_qc = qc.bind_parameters(subs_dic) >>> print(bound_qc) :: ┌──────┐ qb_0: ┤ P(0) ├ ├──────┤ qb_1: ┤ P(1) ├ ├──────┤ qb_2: ┤ P(2) ├ └──────┘ """ subs_circ = self.clearcopy() missing_parameters = self.abstract_params - set(subs_dic.keys()) if missing_parameters: raise Exception( "Need parameter specification for abstract parameters " + str(missing_parameters) ) for ins in self.data: if len(ins.op.abstract_params): op = ins.op.bind_parameters(subs_dic) else: op = ins.op.copy() subs_circ.data.append( Instruction(op, ins.qubits, ins.clbits) ) subs_circ.abstract_params = {} return subs_circ
[docs] def to_latex(self, **kwargs): """ Deploys the Qiskit circuit drawer to generate LaTeX output. Parameters ---------- **kwargs : dict Dictionary of keyword args for Qiskits `circuit_drawer <https://qiskit.org/documentation/stable/0.19/stubs/qiskit.visualization.circuit_drawer.html>`_ function. Returns ------- string A string containing the latex code. """ from qrisp.interface import convert_to_qiskit qiskit_qc = convert_to_qiskit(self, "qiskit", transpile=False) from qiskit.visualization import circuit_drawer return circuit_drawer(qiskit_qc, output="latex_source", **kwargs)
[docs] def qasm(self, formatted=False, filename=None, encoding=None): """ Returns the `OpenQASM <https://en.wikipedia.org/wiki/OpenQASM>`_ string of self. Parameters ---------- formatted : bool, optional Return formatted Qasm string. The default is False. filename : string, optional Save Qasm to file with name ‘filename’. The default is None. encoding : TYPE, optional Optionally specify the encoding to use for the output file if filename is specified. By default, this is set to the system’s default encoding (i.e. whatever locale.getpreferredencoding() returns) and can be set to any valid codec or alias from stdlib’s codec module. Returns ------- string The OPENQASM string. """ try: return self.to_qiskit().qasm(formatted, filename, encoding) except: from qiskit.qasm2 import dumps return dumps( self.to_qiskit())
[docs] def depth(self, depth_indicator = lambda x : 1, transpile=True): """ Returns the depth of the QuantumCircuit. Note that the depth on QuantumCircuit which are not transpiled, might have very little correlation with the runtime. Parameters ---------- depth_indicator : function, optional A function which receives an :ref:`Operation` instance and returns the time/logical depth this operation takes. By default every Operation takes logical depth 1. transpile : bool, optional Boolean to indicate wether the QuantumCircuit should be transpiled before the depth is calculated. The default is True. Returns ------- integer The depth of the QuantumCircuit. """ from qrisp.misc import get_depth_dic if len(self.data) == 0: return 0 depth_dic = get_depth_dic(self, transpile_qc=transpile, depth_indicator = depth_indicator) return max(depth_dic.values())
[docs] def t_depth(self, epsilon = None): r""" Estimates the T-depth of self. T-depth is an important metric for fault tolerant quantum computing, because T gates are expected to be the bottleneck in fault tolerant architectures. According to `this paper <https://arxiv.org/abs/1403.2975>`_, the synthesis of an $RZ(\phi)$ up to precision $\epsilon$ requires $3\text{log}_2(\frac{1}{\epsilon})$ T-gates. Based on this formula, this method performs a conservative estimate of the T-depth of self. Parameters ---------- epsilon : float, optional The precision up to which parametrized gates should be approximated. If not given, Qrisp will determine the parameter with the highest precision. For more information on this feature see the examples Returns ------- float The estimated T-depth. Examples -------- We create a QuantumCircuit and evaluate the T-depth: >>> import numpy as np >>> from qrisp import QuantumCircuit >>> qc = QuantumCircuit(2) >>> qc.t(0) >>> qc.cx(0,1) >>> qc.rx(2*np.pi*3/2**4, 1) >>> qc.t_depth(epsilon = 2**-5) 16 In this example we execute a T-Gate on the 0-th qubit (T-depth : 1) followed by a CNOT gate (T-depth: 0) on qubits 0 and 1 and finally an RX gate. The RX-Gate can be executed by using the decomposition .. math:: RX(\phi) = \text{H } \text{RZ}(\phi) \text{ H} The T-depth of executing a parametrized RX gate is therefore the same as the parametrized RZ. To determine the T-depth of the RZ-gate, executed with precision $2^{-5}$ we use the above formula: .. math:: \begin{align} \text{TDEPTH}(\text{RZ}(\phi), \epsilon = 2^-5) = 3 \text{log}_2(2^5)\\ &= 15 \end{align} We therefore arrive at 16. **Automatic precision determination** In the case of unknown precision, Qrisp assumes that every parameter in the circuit is of the form. .. math:: \phi = 2\pi\frac{m}{2^k} Where $m$ is an integer. Qrisp will then determine the parameter with the maximum $k$ and set $\epsilon = 2^{k_{max} +3}$. >>> qc.t_depth() 22 In this circuit $k_{max} = 4$ therefore, $\epsilon = 2^{-7}$ implying the T-depth is 22. """ if epsilon is None: transpiled_qc = self.transpile() max_circuit_prec = 15 for instr in transpiled_qc.data: op = instr.op for par in op.params: par = int(np.round((par%(2*np.pi))/(2*np.pi)*2**15)) for i in range(max_circuit_prec): if par%(2**i): max_circuit_prec = i break max_circuit_prec = 16 - max_circuit_prec epsilon = 2**(-max_circuit_prec - 3) from qrisp.misc.utility import t_depth_indicator return self.depth(depth_indicator = lambda x : t_depth_indicator(x, epsilon))
[docs] def cnot_depth(self): """ This function returns the CNOT-depth of an self. In NISQ-era devices, CNOT gates are the restricting bottleneck for quantum circuit execution. This function can be used as a gate-speed specifier for the :meth:`compile <qrisp.QuantumSession.compile>` method. Returns ------- int The CNOT depth of self. Examples -------- We create a QuantumCircuit and evaluate it's CNOT depth. >>> from qrisp import QuantumCircuit >>> qc = QuantumCircuit(4) >>> qc.cx(0,1) >>> qc.x(1) >>> qc.cx(1,2) >>> qc.y(2) >>> qc.cx(2,3) >>> qc.cx(1,0) >>> print(qc) ┌───┐ qb_59: ──■────────────┤ X ├───── ┌─┴─┐┌───┐ └─┬─┘ qb_60: ┤ X ├┤ X ├──■────■─────── └───┘└───┘┌─┴─┐┌───┐ qb_61: ──────────┤ X ├┤ Y ├──■── └───┘└───┘┌─┴─┐ qb_62: ────────────────────┤ X ├ └───┘ >>> qc.cnot_depth() 3 """ from qrisp.misc.utility import cnot_depth_indicator return self.depth(depth_indicator = cnot_depth_indicator)
[docs] def num_qubits(self): """ Returns the amount of qubits. Returns ------- int Amount of Qubits. """ return len(self.qubits)
# Interface for appending instructions # Can take either instruction or operations objects # Can apply multiple operations, if given the correct qubits # For instance if it is required to apply an x gate to qubits 4,5,6 execute # qc.append(XGate(), [qubit4, qubit5, qubit6]) # If it is required to apply a cx gate to the qubit pairs (1,2), (3,4), (5,6) # execute qc.append(CXGate(), [[qubit1, qubit3, qubit5], [qubit2, qubit4, qubit6]]) # If it is required to apply a cx gate to the qubit pairs (1,2), (1,3), (1,4) # execute qc.append(CXGate(), [qubit_1, [qubit2, qubit3, qubit4]])
[docs] def append(self, operation_or_instruction, qubits=[], clbits=[]): r""" Method for appending Operation or Instruction objects to the QuantumCircuit. The parameter qubits can be an integer, a list of integers, a Qubit object or a list of Qubit objects. The same is valid for the clbit parameter. If given an Instruction object instead of an Operation, the given qubit and clbit parameters are ignored. Parameters ---------- operation_or_instruction : Operation or Instruction The operation or instruction to be appended to the QuantumCircuit. qubits : integer, list[integer], Qubit, list[Qubit], optional The qubits on which to apply the operation. The default is []. clbits : integer, list[integer], Clbit, list[Clbit], optional The classical bits on which to apply the operation. The default is []. Examples -------- We create a $H^{\otimes 4}$ gate and append it to every second qubit of another QuantumCircuit: >>> from qrisp import QuantumCircuit >>> multi_h_qc = QuantumCircuit(4, name = "multi h") >>> multi_h_qc.h(range(4)) >>> multi_h = multi_h_qc.to_gate() >>> qc = QuantumCircuit(8) >>> qc.append(multi_h, [2*i for i in range(4)]) >>> print(qc) :: ┌──────────┐ qb_4: ┤0 ├ │ │ qb_5: ┤ ├ │ │ qb_6: ┤1 ├ │ │ qb_7: ┤ multi h ├ │ │ qb_8: ┤2 ├ │ │ qb_9: ┤ ├ │ │ qb_10: ┤3 ├ └──────────┘ qb_11: ──────────── """ # Check the type of the instruction/operation # from qrisp.circuit import Instruction, Operation if self.xla_mode > 0: if isinstance(operation_or_instruction, Instruction): self.data.append(operation_or_instruction) else: if self.xla_mode <= 1: if not isinstance(qubits, list): raise Exception(f"Operation {operation_or_instruction.name} was appended with {qubits} in accelerated compilation mode (allowed is type List[Qubit]).") for qb in qubits: if not isinstance(qb, Qubit): raise Exception(f"Operation {operation_or_instruction.name} was appended with {qubits} in accelerated compilation mode (allowed is type List[Qubit]).") self.data.append(Instruction(operation_or_instruction, qubits, clbits)) return if isinstance(operation_or_instruction, Instruction): instruction = operation_or_instruction self.append(instruction.op, instruction.qubits, instruction.clbits) return elif isinstance(operation_or_instruction, Operation): operation = operation_or_instruction else: raise Exception( "Tried to append object type " + str(type(operation_or_instruction)) + " which is neither Instruction nor Operation" ) # Convert arguments (possibly integers) to list # The logic here is that the list structure gets preserved ie. # [[0, 1] ,2] ==> [[qubit_0, qubit_1], qubit_2] # unless the input is a single qubit/integer. # In this case we have # qubit_0 ==> [qubit_0] qubits = convert_to_qb_list(qubits, circuit=self) clbits = convert_to_cb_list(clbits, circuit=self) # Now we check which of the arguments is a list # For user convenience we allow to execute multiple gates at the same time # This comes with some restrictions where the operation to execute could be # ambigous. # When appending n gates with a single call of this function, # each qubit argument must either be a list of n qubits or a single qubit # First we check which arguments are lists qb_argument_is_list = [] for i in range(len(qubits)): if isinstance(qubits[i], list): qb_argument_is_list.append(i) # Same with classical bits cb_argument_is_list = [] for i in range(len(clbits)): if isinstance(clbits[i], list): cb_argument_is_list.append(i) if qb_argument_is_list + cb_argument_is_list: # Determine the amount of gates to be applied if qb_argument_is_list: arg_list_len = len(qubits[qb_argument_is_list[0]]) else: arg_list_len = len(clbits[cb_argument_is_list[0]]) # Check that indeed every list argument that has been given has # arg_list_len entries for arg_list_index in qb_argument_is_list: if len(qubits[arg_list_index]) != arg_list_len: raise Exception( "Don't know how to combine appending arguments " + str((qubits + clbits)) ) for arg_list_index in cb_argument_is_list: if len(clbits[arg_list_index]) != arg_list_len: raise Exception( "Don't know how to combine appending arguments " + str((qubits + clbits)) ) # Create argument constellations for i in range(arg_list_len): qubit_constellation = [] for j in range(len(qubits)): if j in qb_argument_is_list: qubit_constellation.append(qubits[j][i]) else: qubit_constellation.append(qubits[j]) clbit_constellation = [] for j in range(len(clbits)): if j in cb_argument_is_list: clbit_constellation.append(clbits[j][i]) else: clbit_constellation.append(clbits[j]) # Append instruction (qubit_constellation and clbit_constellation) now # contains no lists but only qubit/clbit arguments QuantumCircuit.append(self, operation, qubit_constellation, clbit_constellation) return if len(qubits) != operation.num_qubits: raise Exception( f"Provided incorrect amount ({len(qubits)}) of qubits for operation " + str(operation.name) + f" (requires {operation.num_qubits})" ) if len(clbits) != operation.num_clbits: raise Exception( f"Provided incorrect amount ({len(clbits)}) of clbits for operation " + str(operation.name) + f" (requires {operation.num_clbits})" ) if len(set(qubits)) != len(qubits): raise Exception( f"Duplicate qubit arguments in {qubits} for operation {operation.name}" ) # Building up the list of identifiers seems to slow down this function # We therefore check first if the qubit objects match and if this is not the # case we check if the identifiers match if not set(qubits).issubset(set(self.qubits)): op_identifiers = [qb.identifier for qb in qubits] qc_identifiers = [qb.identifier for qb in self.qubits] if not set(op_identifiers).issubset(qc_identifiers): raise Exception( "Instruction Qubits " + str(set(qubits) - set(self.qubits)) + " not present in circuit" ) else: qubits = [ self.qubits[qc_identifiers.index(op_id)] for op_id in op_identifiers ] if len(set([cb.identifier for cb in clbits])) != len(clbits): raise Exception("Duplicate clbit arguments") if not set([cb.identifier for cb in clbits]).issubset( set([cb.identifier for cb in self.clbits]) ): raise Exception("Instruction Clbits not present in circuit") # Log which abstract parameters have been added to the circuit try: self.abstract_params.update(operation.abstract_params) except AttributeError: pass critical_qubits = [] perm_critical_qubits = [] for qb in qubits: if qb.lock: critical_qubits.append(qb) if qb.perm_lock: perm_critical_qubits.append(qb) critical_qubits = [qb for qb in qubits if qb.lock] if critical_qubits: if critical_qubits[0].lock_message: raise Exception(critical_qubits[0].lock_message) else: raise Exception( f"Tried to perform operation {operation.name}" "on locked qubit {critical_qubits[0]}" ) # Check if there are non-permeable operations on pt_locked qubits critical_qubits = [qb for qb in qubits if qb.perm_lock] if critical_qubits: from qrisp.permeability import is_permeable critical_qubit_indices = [qubits.index(qb) for qb in critical_qubits] if not is_permeable(operation, critical_qubit_indices): if critical_qubits[0].perm_lock_message: raise Exception(critical_qubits[0].perm_lock_message) else: raise Exception( f"Tried to perform non-permeable operation {operation.name} on" f" perm_locked qubit {critical_qubits[0]}" ) self.data.append(Instruction(operation, qubits, clbits))
[docs] def run(self, shots=None, backend=None): """ Runs a QuantumCircuit on a given backend. Parameters ---------- shots : int, optional The amount of shots to perform. The default is 10000. backend : BackendClient, optional The backend on which to evaluate the QuantumCircuit. The default is None. Returns ------- dict The resulting counts for the given QuantumCircuit. Examples -------- We create a GHZ QuantumCircuit and evaluate the results. >>> from qrisp import QuantumCircuit >>> qc = QuantumCircuit(5) >>> qc.h(0) >>> qc.cx(0, range(1,5)) >>> qc.measure(range(5)) >>> qc.run() {'0': 5000, '1': 5000} """ if backend is None: from qrisp.default_backend import def_backend backend = def_backend return backend.run(self, shots)
[docs] def statevector_array(self): """ Performs a simulation of the statevector of self and returns a numpy array of complex numbers. .. note:: Qrisps qubit ordering convention is reversed when compared to Qiskit, because of simulation efficiency reasons. As a rule of thumb you can remember: The statevector array of the following circuit has the amplitude 1 at the index ``0010 = 2`` :: qb.0: ───── qb.1: ───── ┌───┐ qb.2: ┤ X ├ └───┘ qb.3: ───── Returns ------- numpy.ndarray The statevector of this circuit. Examples -------- We create a QuantumCircuit, perform some operations and retrieve the statevector array. >>> from qrisp import QuantumCircuit >>> qc = QuantumCircuit(4) >>> qc.h(qc.qubits) >>> qc.z(-1) >>> qc.statevector_array() array([ 0.24999997+0.j, -0.24999997+0.j, 0.24999997+0.j, -0.24999997+0.j, 0.24999997+0.j, -0.24999997+0.j, 0.24999997+0.j, -0.24999997+0.j, 0.24999997+0.j, -0.24999997+0.j, 0.24999997+0.j, -0.24999997+0.j, 0.24999997+0.j, -0.24999997+0.j, 0.24999997+0.j, -0.24999997+0.j], dtype=complex64) """ from qrisp.simulator import statevector_sim return statevector_sim(self)
def __hash__(self): from hashlib import sha256 res = 0 def hash_(x): temp = str(x).encode("utf-8") hex_value = sha256(temp).hexdigest() return int(hex_value, 16) transpiled_qc = self n = len(self.qubits) for i in range(len(transpiled_qc.data)): instr = transpiled_qc.data[i] qubit_indices = {} for j in range(n): try: qubit_indices[instr.qubits.index(self.qubits[j])] = j except ValueError: pass qubit_indices = [qubit_indices[j] for j in range(len(instr.qubits))] index_hash = hash(tuple(qubit_indices)) params = [] for j in range(len(instr.op.params)): p = hash((instr.op.params[j], i)) params.append(p) param_hash = hash(tuple(params)) if instr.op.definition: op_hash = hash(instr.op.definition) else: op_hash = hash(instr.op.name) res += hash((index_hash, param_hash, op_hash)) * (i + 1) ** 2 res *= len(self.qubits) ** 2 return hash(res)
[docs] @classmethod def from_qasm_str(self, qasm_string): """ Loads a QuantumCircuit from a QASM String. Parameters ---------- qasm_string : string A string obeying the syntax of the OpenQASM specification. Returns ------- QuantumCircuit The corresponding QuantumCircuit. """ from qiskit import QuantumCircuit qiskit_qc = QuantumCircuit().from_qasm_str(qasm_string) from qrisp import QuantumCircuit return QuantumCircuit.from_qiskit(qiskit_qc)
[docs] @classmethod def from_qasm_file(self, filename): """ Loads a QuantumCircuit from a QASM file. Parameters ---------- filename : string A string pointing to a file obeying the OpenQASM syntax. Returns ------- QuantumCircuit The corresponding QuantumCircuit. """ from qiskit import QuantumCircuit qiskit_qc = QuantumCircuit().from_qasm_file(filename) from qrisp import QuantumCircuit return QuantumCircuit.from_qiskit(qiskit_qc)
[docs] @classmethod def from_qiskit(self, qiskit_qc): """ Class method to create QuantumCircuits from Qiskit QuantumCircuits. Parameters ---------- qiskit_qc : Qiskit QuantumCircuit The Qiskit QuantumCircuit to convert. Returns ------- QuantumCircuit The converted QuantumCircuit. Examples -------- We construct a fan-out QuantumCircuit in Qiskit: >>> from qiskit import QuantumCircuit as QiskitQuantumCircuit >>> qc_2 = QiskitQuantumCircuit(4) >>> qc_2.cx(0, range(1,4)) >>> print(qc_2) :: q_0: ──■────■────■── ┌─┴─┐ │ │ q_1: ┤ X ├──┼────┼── └───┘┌─┴─┐ │ q_2: ─────┤ X ├──┼── └───┘┌─┴─┐ q_3: ──────────┤ X ├ └───┘ Note that we don't need to create a QuantumCircuit object first as this is a class method. >>> qrisp_qc_2 = QuantumCircuit.from_qiskit(qc_2) >>> print(qrisp_qc_2) :: qb_8: ──■────■────■── ┌─┴─┐ │ │ qb_9: ┤ X ├──┼────┼── └───┘┌─┴─┐ │ qb_10: ─────┤ X ├──┼── └───┘┌─┴─┐ qb_11: ──────────┤ X ├ └───┘ """ from qrisp.interface import convert_from_qiskit return convert_from_qiskit(qiskit_qc)
[docs] def to_qiskit(self): """ Method to convert the given QuantumCircuit to a Qiskit QuantumCircuit. Returns ------- Qiskit QuantumCircuit The converted circuit. """ from qrisp.interface import convert_to_qiskit return convert_to_qiskit(self, transpile=False)
[docs] def to_pennylane(self): """ Method to convert the given QuantumCircuit to a `Pennylane <https://pennylane.ai/>`_ Circuit. Returns ------- function A function representing a pennylane QuantumCircuit. """ from qrisp.interface import qml_converter return qml_converter(self)
[docs] def to_pytket(self): """ Method to convert the given QuantumCircuit to a `PyTket <https://cqcl.github.io/tket/pytket/api/#>`_ Circuit. Returns ------- function A function representing a pennylane QuantumCircuit. """ from qrisp.interface import pytket_converter return pytket_converter(self)
# Several methods to apply the standard operation defined in standard_operations.py
[docs] def measure(self, qubits, clbits=None): """ Instructs a measurement. If given no classical bits, the proper amount will be created. Parameters ---------- qubits : Qubit The Qubit to be measured. clbits : ClBit, optional The Clbit to store the measurement result. The default is None. """ if clbits is None: from qrisp import QuantumVariable if isinstance(qubits, (list, QuantumVariable)): clbits = [] for i in range(len(qubits)): clbits.append(self.add_clbit()) else: clbits = self.add_clbit() self.append(ops.Measurement(), [qubits], [clbits])
[docs] def cx(self, qubits_0, qubits_1): """ Instruct a CX-gate. Parameters ---------- qubits_0 : Qubit The Qubit to control on. qubits_1 : Qubit The target Qubit. """ self.append(ops.CXGate(), [qubits_0, qubits_1])
[docs] def cy(self, qubits_0, qubits_1): """ Instruct a CY-gate. Parameters ---------- qubits_0 : Qubit The Qubit to control on. qubits_1 : Qubit The target Qubit. """ self.append(ops.CYGate(), [qubits_0, qubits_1])
[docs] def cz(self, qubits_0, qubits_1): """ Instruct a CZ-gate. Parameters ---------- qubits_0 : Qubit The Qubit to control on. qubits_1 : Qubit The target Qubit. """ self.append(ops.CZGate(), [qubits_0, qubits_1])
[docs] def h(self, qubits): """ Instruct a Hadamard-gate. Parameters ---------- qubits : Qubit The Qubit to apply the gate on. """ self.append(ops.HGate(), [qubits])
[docs] def x(self, qubits): """ Instruct a Pauli-X-gate. Parameters ---------- qubits : Qubit The Qubit to apply the gate on. """ self.append(ops.XGate(), [qubits])
[docs] def y(self, qubits): """ Instruct a Pauli-Y-gate. Parameters ---------- qubits : Qubit The Qubit to apply the gate on. """ self.append(ops.YGate(), [qubits])
[docs] def z(self, qubits): """ Instruct a Pauli-Z-gate. Parameters ---------- qubits : Qubit The Qubit to apply the gate on. """ self.append(ops.ZGate(), [qubits])
[docs] def rx(self, phi, qubits): """ Instruct a parametrized RX-gate. Parameters ---------- phi : float or sympy.Symbol The angle parameter. qubits : Qubit The Qubit to apply the gate on. """ if phi == 0: return self.append(ops.RXGate(phi), [qubits])
[docs] def ry(self, phi, qubits): """ Instruct a parametrized RY-gate. Parameters ---------- phi : float or sympy.Symbol The angle parameter. qubits : Qubit The Qubit to apply the gate on. """ if phi == 0: return self.append(ops.RYGate(phi), [qubits])
[docs] def rz(self, phi, qubits): """ Instruct a parametrized RZ-gate. Parameters ---------- phi : float or sympy.Symbol The angle parameter. qubits : Qubit The Qubit to apply the gate on. """ if phi == 0: return self.append(ops.RZGate(phi), [qubits])
[docs] def cp(self, phi, qubits_0, qubits_1): """ Instruct a controlled phase-gate. Parameters ---------- phi : float or sympy.Symbol The angle parameter. qubits_0 : Qubit The Qubit to apply the gate on. qubits_1 : Qubit The other Qubit to apply the gate on. """ if phi == 0: return self.append(ops.CPGate(phi), [qubits_0, qubits_1])
[docs] def p(self, phi, qubits): """ Instruct a phase-gate. Parameters ---------- phi : float or sympy.Symbol The angle parameter. qubits : Qubit The Qubit to apply the gate on. """ if phi == 0: return self.append(ops.PGate(phi), [qubits])
[docs] def rxx(self, phi, qubits_0, qubits_1): """ Instruct an RXX-gate. Parameters ---------- phi : float or sympy.Symbol The angle parameter. qubits_0 : Qubit The Qubit to apply the gate on. qubits_1 : Qubit The other Qubit to apply the gate on. """ if phi == 0: return self.append(ops.RXXGate(phi), [qubits_0, qubits_1])
[docs] def rzz(self, phi, qubits_0, qubits_1): """ Instruct an RZZ-gate. Parameters ---------- phi : float or sympy.Symbol The angle parameter. qubits_0 : Qubit The Qubit to apply the gate on. qubits_1 : Qubit The other Qubit to apply the gate on. """ if phi == 0: return self.append(ops.RZZGate(phi), [qubits_0, qubits_1])
[docs] def xxyy(self, phi, beta, qubits_0, qubits_1): """ Instruct an XXYY-gate. Parameters ---------- phi : float or sympy.Symbol The angle parameter. beta : float or sympi.Symbol The other angle parameter qubits_0 : Qubit The Qubit to apply the gate on. qubits_1 : Qubit The other Qubit to apply the gate on. """ if phi == 0: return self.append(ops.XXYYGate(phi, beta), [qubits_0, qubits_1])
[docs] def swap(self, qubits_0, qubits_1): """ Instruct a SWAP-gate. Parameters ---------- qubits_0 : Qubit The qubit to swap. qubits_1 : Qubit The other qubit to swap. """ self.append(ops.SwapGate(), [qubits_0, qubits_1])
[docs] def mcx(self, control_qubits, target_qubits, method="gray", ctrl_state=-1): """ Instruct a multi-controlled X-gate. Parameters ---------- control_qubits : list The list of Qubits to control on. target_qubits : Qubit The target Qubit. method : str, optional The algorithm to synthesize the mcx gate. The default is "gray". ctrl_state : str or int, optional The state on which the X gate is activated. Can be supplied as a string (i.e. "010110...") or an integer. The default is all ones ("11111..."). """ self.append( ops.MCXGate(len(control_qubits), ctrl_state=ctrl_state, method=method), control_qubits + [target_qubits], )
[docs] def ccx(self, ctrl_qubit_0, ctrl_qubit_1, target_qubit, method="gray"): """ Instruct a Toffoli-gate. Parameters ---------- ctrl_qubit_0 : list The first control Qubit. ctrl_qubit_1 : Qubit The second control Qubit. target_qubit : Qubit. The target Qubit. method : str, optional The algorithm to synthesize the mcx gate. The default is "gray". """ self.mcx([ctrl_qubit_0, ctrl_qubit_1], target_qubit, method=method)
def crx(self, phi, qubits_0, qubits_1): """ Instruct a controlled rx-gate. Parameters ---------- phi : float or sympy.Symbol The angle parameter. qubits_0 : Qubit The Qubit to apply the gate on. qubits_1 : Qubit The other Qubit to apply the gate on. """ if phi == 0: return self.append(ops.MCRXGate(phi, 1), [qubits_0, qubits_1])
[docs] def t(self, qubits): """ Instruct a T-gate. Parameters ---------- qubits : Qubit The Qubit to apply the gate on. """ self.append(ops.TGate(), [qubits])
[docs] def t_dg(self, qubits): """ Instruct a dagger T-gate. Parameters ---------- qubits : Qubit The Qubit to apply the gate on. """ self.append(ops.TGate().inverse(), [qubits])
[docs] def s(self, qubits): """ Instruct an S-gate. Parameters ---------- qubits : Qubit The Qubit to apply the gate on. """ self.append(ops.SGate(), [qubits])
[docs] def s_dg(self, qubits): """ Instruct a daggered S-gate. Parameters ---------- qubits : Qubit The Qubit to apply the gate on. """ self.append(ops.SGate().inverse(), [qubits])
[docs] def sx(self, qubits): """ Instruct a SX-gate. Parameters ---------- qubits : Qubit The Qubit to apply the gate on. """ self.append(ops.SXGate(), [qubits])
[docs] def sx_dg(self, qubits): """ Instruct a daggered SX-gate. Parameters ---------- qubits : Qubit The Qubit to apply the gate on. """ self.append(ops.SXGate().inverse(), [qubits])
def barrier(self, qubits=None, clbits=None): """ Instruct a Barrier onto the given Qubit. Barriers can be used as visual markers and compiler directives. Parameters ---------- qubits : Qubit The Qubit to apply the barrier on. clbits : Clbit The Clbits to apply the barrier on. """ if qubits is None: qubits = self.qubits self.append(ops.Barrier(len(qubits)), qubits)
[docs] def reset(self, qubits): r""" Instruct a reset. This resets this Qubit into the $\ket{0}$ state regardless of its previous state. Parameters ---------- qubits : Qubit The Qubit to reset. """ self.append(ops.Reset(), [qubits])
[docs] def u3(self, theta, phi, lam, qubits): r""" Instruct a U3-gate from given Euler angles. A U3 gate has the unitary: .. math:: U3(\theta, \phi, \lambda) = \begin{pmatrix} \cos{(\frac{\theta}{2})} & -\exp{(i\lambda)}\sin{(\frac{\theta}{2})} \\ \exp{(i\phi)} \sin{(\frac{\theta}{2})} & \exp{(i(\phi+\lambda))}\cos{(\frac{\theta}{2})} \end{pmatrix} Parameters ---------- theta : float or sympy.Symbol The theta parameter. phi : float or sympy.Symbol The phi parameter. lam : float or sympy.Symbol The lambda parameter. qubits : Qubit The Qubit to apply the u3 gate on. """ self.append(ops.u3Gate(theta, phi, lam), [qubits])
[docs] def unitary(self, unitary_array, qubits): """ Instruct a U3-gate from a given U3 matrix. Parameters ---------- unitary_array : numpy.ndarray The U3 matrix to apply. qubits : Qubit The Qubit to apply the gate on. """ mat = unitary_array coeff = 1 / np.sqrt(np.linalg.det(mat)) gphase = -np.angle(coeff) % (2 * np.pi) tmp_10 = np.abs((coeff * mat[1][0])) tmp_00 = np.abs((coeff * mat[0][0])) theta = 2 * np.arctan2(tmp_10, tmp_00) phiplambda2 = np.angle(coeff * mat[1][1]) % (2 * np.pi) phimlambda2 = np.angle(coeff * mat[1][0]) % (2 * np.pi) phi = phiplambda2 + phimlambda2 lam = phiplambda2 - phimlambda2 # gphase -= (phi + lam) arg_max = np.argmax(np.abs(mat).flatten()) from qrisp.simulator.unitary_management import u3matrix temp_u3 = u3matrix(theta, phi, lam, 0).flatten() gphase = (-np.angle(temp_u3[arg_max] / mat.flatten()[arg_max])) % (2 * np.pi) from qrisp.circuit import U3Gate from qrisp.simulator.unitary_management import u3matrix self.append(U3Gate(theta, phi, lam, global_phase=gphase), qubits)
# self.u3(theta, phi, lam, [qubits], global_phase = gphase)
[docs] def gphase(self, phi, qubits): """ Instruct a global phase. Global phases do not directly influence the QuantumCircuits outcome however they can become physical if used as a base gate for a controlled operation. Parameters ---------- phi : float or sympy.Symbol The angle parameter. qubits : TYPE The Qubit to apply the gate on. """ self.append(ops.GPhaseGate(phi), [qubits])
[docs] def id(self, qubits): """ Instruct an identity gate. Identity gates are simply placeholders and have no effect on the quantum state. Parameters ---------- qubits : TYPE The Qubit to apply the gate on. """ self.append(ops.IDGate(), [qubits])
def to_pdag(self, remove_artificials = False): from qrisp.permeability import PermeabilityGraph return PermeabilityGraph(self, remove_artificials = remove_artificials)
# Converts various inputs (eg. integers, qubits or quantum variables) to lists of qubit # used in the append method of QuantumCircuit and QuantumSession def convert_to_qb_list(input, circuit=None, top_level=True): from qrisp import QuantumArray if issubclass(input.__class__, Qubit): if top_level: result = [input] else: result = input elif isinstance(input, QuantumArray): result = sum([qv.reg for qv in input.flatten()], []) elif hasattr(input, "__iter__"): result = [] for i in range(len(input)): result.append(convert_to_qb_list(input[i], circuit, top_level=False)) elif hasattr(input, "reg"): result = list(input.reg) elif isinstance(input, int): if isinstance(circuit, type(None)): raise Exception( "Tried to convert integer argument to qubit without given circuit" ) result = convert_to_qb_list(circuit.qubits[input], top_level=top_level) else: raise Exception("Couldn't convert type " + str(type(input)) + " to qubit list") return result def convert_to_cb_list(input, circuit=None, top_level=True): from qrisp.circuit import Clbit if hasattr(input, "__iter__"): result = [] for i in range(len(input)): result.append(convert_to_cb_list(input[i], circuit, top_level=False)) elif isinstance(input, int): if isinstance(circuit, type(None)): raise Exception( "Tried to convert integer argument to qubit without given circuit" ) result = convert_to_cb_list(circuit.clbits[input], top_level=top_level) elif issubclass(input.__class__, Clbit): if top_level: result = [input] else: result = input return result