def __init__( self, control_wires=None, wires=None, control_values=None, work_wires=None, do_queue=True, ): wires = Wires(wires) control_wires = Wires(control_wires) work_wires = Wires([]) if work_wires is None else Wires(work_wires) if len(wires) != 1: raise ValueError("MultiControlledX accepts a single target wire.") if Wires.shared_wires([wires, work_wires]) or Wires.shared_wires( [control_wires, work_wires]): raise ValueError( "The work wires must be different from the control and target wires" ) self._target_wire = wires[0] self._work_wires = work_wires super().__init__( np.array([[0, 1], [1, 0]]), control_wires=control_wires, wires=wires, control_values=control_values, do_queue=do_queue, )
def test_shared_wires_method(self): """Tests the ``shared_wires()`` method.""" wires1 = Wires([4, 0, 1]) wires2 = Wires([3, 0, 4]) wires3 = Wires([4, 0]) res = Wires.shared_wires([wires1, wires2, wires3]) assert res == Wires([4, 0]) res = Wires.shared_wires([wires2, wires1, wires3]) assert res == Wires([0, 4]) with pytest.raises(WireError, match="Expected a Wires object"): Wires.shared_wires([[3, 4], [8, 5]])
def __matmul__(self, H): r"""The tensor product operation between a Hamiltonian and a Hamiltonian/Tensor/Observable.""" coeffs1 = self.coeffs.copy() terms1 = self.ops.copy() if isinstance(H, Hamiltonian): shared_wires = Wires.shared_wires([self.wires, H.wires]) if len(shared_wires) > 0: raise ValueError( "Hamiltonians can only be multiplied together if they act on " "different sets of wires") coeffs2 = H.coeffs terms2 = H.ops coeffs = [c[0] * c[1] for c in itertools.product(coeffs1, coeffs2)] term_list = itertools.product(terms1, terms2) terms = [qml.operation.Tensor(t[0], t[1]) for t in term_list] return qml.Hamiltonian(coeffs, terms, simplify=True) if isinstance(H, (Tensor, Observable)): coeffs = coeffs1 terms = [term @ H for term in terms1] return qml.Hamiltonian(coeffs, terms, simplify=True) raise ValueError(f"Cannot tensor product Hamiltonian and {type(H)}")
def _commute_controlled_left(op_list): """Push commuting single qubit gates to the left of controlled gates. Args: op_list (list[Operation]): The initial list of operations. Returns: list[Operation]: The modified list of operations with all single-qubit gates as far left as possible. """ # We will go through the list forwards; whenever we find a single-qubit # gate, we will extract it and push it through 2-qubit gates as far as # possible back to the left. current_location = 0 while current_location < len(op_list): current_gate = op_list[current_location] if current_gate.basis is None or len(current_gate.wires) != 1: current_location += 1 continue # Pass a backwards copy of the list prev_gate_idx = find_next_gate(current_gate.wires, op_list[:current_location][::-1]) new_location = current_location while prev_gate_idx is not None: prev_gate = op_list[new_location - prev_gate_idx - 1] if prev_gate.basis is None: break if len(prev_gate.control_wires) == 0: break shared_controls = Wires.shared_wires( [Wires(current_gate.wires), prev_gate.control_wires] ) if len(shared_controls) > 0: if current_gate.basis == "Z": new_location = new_location - prev_gate_idx - 1 else: break else: if current_gate.basis == prev_gate.basis: new_location = new_location - prev_gate_idx - 1 else: break prev_gate_idx = find_next_gate(current_gate.wires, op_list[:new_location][::-1]) op_list.pop(current_location) op_list.insert(new_location, current_gate) current_location += 1 return op_list
def __init__(self, unitary, target_wires, estimation_wires, do_queue=True): self.target_wires = Wires(target_wires) self.estimation_wires = Wires(estimation_wires) wires = self.target_wires + self.estimation_wires if len(Wires.shared_wires([self.target_wires, self.estimation_wires ])) != 0: raise qml.QuantumFunctionError( "The target wires and estimation wires must be different") super().__init__(unitary, wires=wires, do_queue=do_queue)
def __init__( self, *params, control_wires=None, wires=None, control_values=None, do_queue=True, ): if control_wires is None: raise ValueError("Must specify control wires") wires = Wires(wires) control_wires = Wires(control_wires) if Wires.shared_wires([wires, control_wires]): raise ValueError( "The control wires must be different from the wires specified to apply the unitary on." ) U = params[0] target_dim = 2**len(wires) if len(U) != target_dim: raise ValueError( f"Input unitary must be of shape {(target_dim, target_dim)}") # Saving for the circuit drawer self._target_wires = wires self._control_wires = control_wires self.U = U wires = control_wires + wires # If control values unspecified, we control on the all-ones string if not control_values: control_values = "1" * len(control_wires) control_int = self._parse_control_values(control_wires, control_values) self.control_values = control_values # A multi-controlled operation is a block-diagonal matrix partitioned into # blocks where the operation being applied sits in the block positioned at # the integer value of the control string. For example, controlling a # unitary U with 2 qubits will produce matrices with block structure # (U, I, I, I) if the control is on bits '00', (I, U, I, I) if on bits '01', # etc. The positioning of the block is controlled by padding the block diagonal # to the left and right with the correct amount of identity blocks. self._padding_left = control_int * len(U) self._padding_right = 2**len(wires) - len(U) - self._padding_left self._CU = None super().__init__(*params, wires=wires, do_queue=do_queue)
def __init__( self, *params, control_wires=None, wires=None, control_values=None, work_wires=None, do_queue=True, ): wires = Wires(wires) control_wires = Wires(control_wires) work_wires = Wires([]) if work_wires is None else Wires(work_wires) if len(wires) != 1: raise ValueError("MultiControlledX accepts a single target wire.") if Wires.shared_wires([wires, work_wires]) or Wires.shared_wires( [control_wires, work_wires]): raise ValueError( "The work wires must be different from the control and target wires" ) self._target_wire = wires[0] self._work_wires = work_wires self._control_wires = control_wires wires = control_wires + wires if not control_values: control_values = "1" * len(control_wires) control_int = self._parse_control_values(control_wires, control_values) self.control_values = control_values self._padding_left = control_int * 2 self._padding_right = 2**len(wires) - 2 - self._padding_left self._CX = None super().__init__(*params, wires=wires, do_queue=do_queue)
def extract_active_wires(self, raw_operation_grid, raw_observable_grid): """Get the subset of wires on the device that are used in the circuit. Args: raw_operation_grid (Iterable[~.Operator]): The raw grid of operations raw_observable_grid (Iterable[~.Operator]): The raw grid of observables Return: Wires: active wires on the device """ # pylint: disable=protected-access all_operators = list(qml.utils._flatten(raw_operation_grid)) + list( qml.utils._flatten(raw_observable_grid) ) all_wires_with_duplicates = [op.wires for op in all_operators if op is not None] # make Wires object containing all used wires all_wires = Wires.all_wires(all_wires_with_duplicates) # shared wires will observe the ordering of the device's wires shared_wires = Wires.shared_wires([self.wires, all_wires]) return shared_wires
def find_next_gate(wires, op_list): """Given a list of operations, finds the next operation that acts on at least one of the same set of wires, if present. Args: wires (Wires): A set of wires acted on by a quantum operation. op_list (list[Operation]): A list of operations that are implemented after the operation that acts on ``wires``. Returns: int or None: The index, in ``op_list``, of the earliest gate that uses one or more of the same wires, or ``None`` if no such gate is present. """ next_gate_idx = None for op_idx, op in enumerate(op_list): if len(Wires.shared_wires([wires, op.wires])) > 0: next_gate_idx = op_idx break return next_gate_idx
def QuantumPhaseEstimation(unitary, target_wires, estimation_wires): r"""Performs the `quantum phase estimation <https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm>`__ circuit. Given a unitary matrix :math:`U`, this template applies the circuit for quantum phase estimation. The unitary is applied to the qubits specified by ``target_wires`` and :math:`n` qubits are used for phase estimation as specified by ``estimation_wires``. .. figure:: ../../_static/templates/subroutines/qpe.svg :align: center :width: 60% :target: javascript:void(0); This circuit can be used to perform the standard quantum phase estimation algorithm, consisting of the following steps: #. Prepare ``target_wires`` in a given state. If ``target_wires`` are prepared in an eigenstate of :math:`U` that has corresponding eigenvalue :math:`e^{2 \pi i \theta}` with phase :math:`\theta \in [0, 1)`, this algorithm will measure :math:`\theta`. Other input states can be prepared more generally. #. Apply the ``QuantumPhaseEstimation`` circuit. #. Measure ``estimation_wires`` using :func:`~.probs`, giving a probability distribution over measurement outcomes in the computational basis. #. Find the index of the largest value in the probability distribution and divide that number by :math:`2^{n}`. This number will be an estimate of :math:`\theta` with an error that decreases exponentially with the number of qubits :math:`n`. Note that if :math:`\theta \in (-1, 0]`, we can estimate the phase by again finding the index :math:`i` found in step 4 and calculating :math:`\theta \approx \frac{1 - i}{2^{n}}`. The usage details below give an example of this case. Args: unitary (array): the phase estimation unitary, specified as a matrix target_wires (Union[Wires, Sequence[int], or int]): the target wires to apply the unitary estimation_wires (Union[Wires, Sequence[int], or int]): the wires to be used for phase estimation Raises: QuantumFunctionError: if the ``target_wires`` and ``estimation_wires`` share a common element .. UsageDetails:: Consider the matrix corresponding to a rotation from an :class:`~.RX` gate: .. code-block:: python import pennylane as qml from pennylane.templates import QuantumPhaseEstimation from pennylane import numpy as np phase = 5 target_wires = [0] unitary = qml.RX(phase, wires=0).matrix The ``phase`` parameter can be estimated using ``QuantumPhaseEstimation``. An example is shown below using a register of five phase-estimation qubits: .. code-block:: python n_estimation_wires = 5 estimation_wires = range(1, n_estimation_wires + 1) dev = qml.device("default.qubit", wires=n_estimation_wires + 1) @qml.qnode(dev) def circuit(): # Start in the |+> eigenstate of the unitary qml.Hadamard(wires=target_wires) QuantumPhaseEstimation( unitary, target_wires=target_wires, estimation_wires=estimation_wires, ) return qml.probs(estimation_wires) phase_estimated = np.argmax(circuit()) / 2 ** n_estimation_wires # Need to rescale phase due to convention of RX gate phase_estimated = 4 * np.pi * (1 - phase_estimated) """ target_wires = Wires(target_wires) estimation_wires = Wires(estimation_wires) if len(Wires.shared_wires([target_wires, estimation_wires])) != 0: raise qml.QuantumFunctionError( "The target wires and estimation wires must be different") unitary_powers = [unitary] for _ in range(len(estimation_wires) - 1): new_power = unitary_powers[-1] @ unitary_powers[-1] unitary_powers.append(new_power) for wire in estimation_wires: qml.Hadamard(wire) qml.ControlledQubitUnitary(unitary_powers.pop(), control_wires=wire, wires=target_wires) qml.QFT(wires=estimation_wires).inv()
def quantum_monte_carlo(fn, wires, target_wire, estimation_wires): r"""Provides the circuit to perform the `quantum Monte Carlo estimation <https://arxiv.org/abs/1805.00109>`__ algorithm. The input ``fn`` should be the quantum circuit corresponding to the :math:`\mathcal{F}` unitary in the paper above. This unitary encodes the probability distribution and random variable onto ``wires`` so that measurement of the ``target_wire`` provides the expectation value to be estimated. The quantum Monte Carlo algorithm then estimates the expectation value using quantum phase estimation (check out :class:`~.QuantumPhaseEstimation` for more details), using the ``estimation_wires``. .. note:: A complementary approach for quantum Monte Carlo is available with the :class:`~.QuantumMonteCarlo` template. The ``quantum_monte_carlo`` transform is intended for use when you already have the circuit for performing :math:`\mathcal{F}` set up, and is compatible with resource estimation and potential hardware implementation. The :class:`~.QuantumMonteCarlo` template is only compatible with simulators, but may perform faster and is suited to quick prototyping. Args: fn (Callable): a quantum function that applies quantum operations according to the :math:`\mathcal{F}` unitary used as part of quantum Monte Carlo estimation wires (Union[Wires or Sequence[int]]): the wires acted upon by the ``fn`` circuit target_wire (Union[Wires, int]): The wire in which the expectation value is encoded. Must be contained within ``wires``. estimation_wires (Union[Wires, Sequence[int], or int]): the wires used for phase estimation Returns: function: The circuit for quantum Monte Carlo estimation Raises: ValueError: if ``wires`` and ``estimation_wires`` share a common wire .. UsageDetails:: Consider an input quantum circuit ``fn`` that performs the unitary .. math:: \mathcal{F} = \mathcal{R} \mathcal{A}. .. figure:: ../../_static/ops/f.svg :align: center :width: 15% :target: javascript:void(0); Here, the unitary :math:`\mathcal{A}` prepares a probability distribution :math:`p(i)` of dimension :math:`M = 2^{m}` over :math:`m \geq 1` qubits: .. math:: \mathcal{A}|0\rangle^{\otimes m} = \sum_{i \in X} p(i) |i\rangle, where :math:`X = \{0, 1, \ldots, M - 1\}` and :math:`|i\rangle` is the basis state corresponding to :math:`i`. The :math:`\mathcal{R}` unitary imprints the result of a function :math:`f: X \rightarrow [0, 1]` onto an ancilla qubit: .. math:: \mathcal{R}|i\rangle |0\rangle = |i\rangle \left(\sqrt{1 - f(i)} |0\rangle + \sqrt{f(i)}|1\rangle\right). Following `this <https://arxiv.org/abs/1805.00109>`__ paper, the probability of measuring the state :math:`|1\rangle` in the final qubit is .. math:: \mu = \sum_{i \in X} p(i) f(i). However, it is possible to measure :math:`\mu` more efficiently using quantum Monte Carlo estimation. This function transforms an input quantum circuit ``fn`` that performs the unitary :math:`\mathcal{F}` to a larger circuit for measuring :math:`\mu` using the quantum Monte Carlo algorithm. .. figure:: ../../_static/ops/qmc.svg :align: center :width: 60% :target: javascript:void(0); The algorithm proceeds as follows: #. The probability distribution :math:`p(i)` is encoded using a unitary :math:`\mathcal{A}` applied to the first :math:`m` qubits specified by ``wires``. #. The function :math:`f(i)` is encoded onto the ``target_wire`` using a unitary :math:`\mathcal{R}`. #. The unitary :math:`\mathcal{Q}` is defined with eigenvalues :math:`e^{\pm 2 \pi i \theta}` such that the phase :math:`\theta` encodes the expectation value through the equation :math:`\mu = (1 + \cos (\pi \theta)) / 2`. The circuit in steps 1 and 2 prepares an equal superposition over the two states corresponding to the eigenvalues :math:`e^{\pm 2 \pi i \theta}`. #. The circuit returned by this function is applied so that :math:`\pm\theta` can be estimated by finding the probabilities of the :math:`n` estimation wires. This in turn allows for the estimation of :math:`\mu`. Visit `Rebentrost et al. (2018) <https://arxiv.org/abs/1805.00109>`__ for further details. In this algorithm, the number of applications :math:`N` of the :math:`\mathcal{Q}` unitary scales as :math:`2^{n}`. However, due to the use of quantum phase estimation, the error :math:`\epsilon` scales as :math:`\mathcal{O}(2^{-n})`. Hence, .. math:: N = \mathcal{O}\left(\frac{1}{\epsilon}\right). This scaling can be compared to standard Monte Carlo estimation, where :math:`N` samples are generated from the probability distribution and the average over :math:`f` is taken. In that case, .. math:: N = \mathcal{O}\left(\frac{1}{\epsilon^{2}}\right). Hence, the quantum Monte Carlo algorithm has a quadratically improved time complexity with :math:`N`. **Example** Consider a standard normal distribution :math:`p(x)` and a function :math:`f(x) = \sin ^{2} (x)`. The expectation value of :math:`f(x)` is :math:`\int_{-\infty}^{\infty}f(x)p(x) \approx 0.432332`. This number can be approximated by discretizing the problem and using the quantum Monte Carlo algorithm. First, the problem is discretized: .. code-block:: python from scipy.stats import norm m = 5 M = 2 ** m xmax = np.pi # bound to region [-pi, pi] xs = np.linspace(-xmax, xmax, M) probs = np.array([norm().pdf(x) for x in xs]) probs /= np.sum(probs) func = lambda i: np.sin(xs[i]) ** 2 r_rotations = np.array([2 * np.arcsin(np.sqrt(func(i))) for i in range(M)]) The ``quantum_monte_carlo`` transform can then be used: .. code-block:: from pennylane.templates.state_preparations.mottonen import ( _uniform_rotation_dagger as r_unitary, ) n = 6 N = 2 ** n a_wires = range(m) wires = range(m + 1) target_wire = m estimation_wires = range(m + 1, n + m + 1) dev = qml.device("default.qubit", wires=(n + m + 1)) def fn(): qml.templates.MottonenStatePreparation(np.sqrt(probs), wires=a_wires) r_unitary(qml.RY, r_rotations, control_wires=a_wires[::-1], target_wire=target_wire) @qml.qnode(dev) def qmc(): qml.quantum_monte_carlo(fn, wires, target_wire, estimation_wires)() return qml.probs(estimation_wires) phase_estimated = np.argmax(qmc()[:int(N / 2)]) / N The estimated value can be retrieved using the formula :math:`\mu = (1-\cos(\pi \theta))/2` >>> (1 - np.cos(np.pi * phase_estimated)) / 2 0.42663476277231915 It is also possible to explore the resources required to perform the quantum Monte Carlo algorithm >>> qtape = qmc.qtape.expand(depth=1) >>> qml.specs(qmc)() {'gate_sizes': defaultdict(int, {1: 15943, 2: 15812, 7: 126, 6: 1}), 'gate_types': defaultdict(int, {'RY': 15433, 'CNOT': 15686, 'Hadamard': 258, 'CZ': 126, 'PauliX': 252, 'MultiControlledX': 126, 'QFT.inv': 1}), 'num_operations': 31882, 'num_observables': 1, 'num_diagonalizing_gates': 0, 'num_used_wires': 12, 'depth': 30610, 'num_device_wires': 12, 'device_name': 'default.qubit.autograd', 'diff_method': 'backprop'} """ wires = Wires(wires) target_wire = Wires(target_wire) estimation_wires = Wires(estimation_wires) if Wires.shared_wires([wires, estimation_wires]): raise ValueError( "No wires can be shared between the wires and estimation_wires registers" ) @wraps(fn) def wrapper(*args, **kwargs): fn(*args, **kwargs) for i, control_wire in enumerate(estimation_wires): Hadamard(control_wire) # Find wires eligible to be used as helper wires work_wires = estimation_wires.toset() - {control_wire} n_reps = 2**(len(estimation_wires) - (i + 1)) q = apply_controlled_Q( fn, wires=wires, target_wire=target_wire, control_wire=control_wire, work_wires=work_wires, ) for _ in range(n_reps): q(*args, **kwargs) QFT(wires=estimation_wires).inv() return wrapper
def cancel_inverses(tape): """Quantum function transform to remove any operations that are applied next to their (self-)inverse. Args: qfunc (function): A quantum function. Returns: function: the transformed quantum function **Example** Consider the following quantum function: .. code-block:: python def qfunc(x, y, z): qml.Hadamard(wires=0) qml.Hadamard(wires=1) qml.Hadamard(wires=0) qml.RX(x, wires=2) qml.RY(y, wires=1) qml.PauliX(wires=1) qml.RZ(z, wires=0) qml.RX(y, wires=2) qml.CNOT(wires=[0, 2]) qml.PauliX(wires=1) return qml.expval(qml.PauliZ(0)) The circuit before optimization: >>> dev = qml.device('default.qubit', wires=3) >>> qnode = qml.QNode(qfunc, dev) >>> print(qml.draw(qnode)(1, 2, 3)) 0: ──H──────H──────RZ(3)─────╭C──┤ ⟨Z⟩ 1: ──H──────RY(2)──X──────X──│───┤ 2: ──RX(1)──RX(2)────────────╰X──┤ We can see that there are two adjacent Hadamards on the first qubit that should cancel each other out. Similarly, there are two Pauli-X gates on the second qubit that should cancel. We can obtain a simplified circuit by running the ``cancel_inverses`` transform: >>> optimized_qfunc = cancel_inverses(qfunc) >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) >>> print(qml.draw(optimized_qnode)(1, 2, 3)) 0: ──RZ(3)─────────╭C──┤ ⟨Z⟩ 1: ──H──────RY(2)──│───┤ 2: ──RX(1)──RX(2)──╰X──┤ """ # Make a working copy of the list to traverse list_copy = tape.operations.copy() while len(list_copy) > 0: current_gate = list_copy[0] # Find the next gate that acts on at least one of the same wires next_gate_idx = find_next_gate(current_gate.wires, list_copy[1:]) # If no such gate is found queue the operation and move on if next_gate_idx is None: apply(current_gate) list_copy.pop(0) continue # Otherwise, get the next gate next_gate = list_copy[next_gate_idx + 1] # There are then three possibilities that may lead to inverse cancellation. For a gate U, # 1. U is self-inverse, and the next gate is also U # 2. The current gate is U.inv and the next gate is U # 3. The current gate is U and the next gate is U.inv # Case 1 are_self_inverses = current_gate.is_self_inverse and current_gate.name == next_gate.name # Cases 2 and 3 are_inverses = False name_set = set([current_gate.name, next_gate.name]) shortest_name = min(name_set) if set([shortest_name, shortest_name + ".inv"]) == name_set: are_inverses = True # If either of the two flags is true, we can potentially cancel the gates if are_self_inverses or are_inverses: # If the wires are the same, then we can safely remove both if current_gate.wires == next_gate.wires: list_copy.pop(next_gate_idx + 1) # If wires are not equal, there are two things that can happen else: # There is not full overlap in the wires; we cannot cancel if len( Wires.shared_wires([ current_gate.wires, next_gate.wires ])) != len(current_gate.wires): apply(current_gate) # There is full overlap, but the wires are in a different order else: # If the wires are in a different order, gates that are "symmetric" # over all wires (e.g., CZ), can be cancelled. if current_gate.is_symmetric_over_all_wires: list_copy.pop(next_gate_idx + 1) # For other gates, as long as the control wires are the same, we can still # cancel (e.g., the Toffoli gate). elif current_gate.is_symmetric_over_control_wires: if (len( Wires.shared_wires([ current_gate.wires[:-1], next_gate.wires[:-1] ])) == len(current_gate.wires) - 1): list_copy.pop(next_gate_idx + 1) else: apply(current_gate) # Apply gate any cases where there is no wire symmetry else: apply(current_gate) # If neither of the flags are true, queue and move on to the next item else: apply(current_gate) # Remove this gate from the working list list_copy.pop(0) # Queue the measurements normally for m in tape.measurements: apply(m)
def _commute_controlled_right(op_list): """Push commuting single qubit gates to the right of controlled gates. Args: op_list (list[Operation]): The initial list of operations. Returns: list[Operation]: The modified list of operations with all single-qubit gates as far right as possible. """ # We will go through the list backwards; whenever we find a single-qubit # gate, we will extract it and push it through 2-qubit gates as far as # possible to the right. current_location = len(op_list) - 1 while current_location >= 0: current_gate = op_list[current_location] # We are looking only at the gates that can be pushed through # controls/targets; these are single-qubit gates with the basis # property specified. if current_gate.basis is None or len(current_gate.wires) != 1: current_location -= 1 continue # Find the next gate that contains an overlapping wire next_gate_idx = find_next_gate(current_gate.wires, op_list[current_location + 1 :]) new_location = current_location # Loop as long as a valid next gate exists while next_gate_idx is not None: next_gate = op_list[new_location + next_gate_idx + 1] # Only go ahead if information is available if next_gate.basis is None: break # If the next gate does not have control_wires defined, it is not # controlled so we can't push through. if len(next_gate.control_wires) == 0: break shared_controls = Wires.shared_wires( [Wires(current_gate.wires), next_gate.control_wires] ) # Case 1: overlap is on the control wires. Only Z-type gates go through if len(shared_controls) > 0: if current_gate.basis == "Z": new_location += next_gate_idx + 1 else: break # Case 2: since we know the gates overlap somewhere, and it's a # single-qubit gate, if it wasn't on a control it's the target. else: if current_gate.basis == next_gate.basis: new_location += next_gate_idx + 1 else: break next_gate_idx = find_next_gate(current_gate.wires, op_list[new_location + 1 :]) # After we have gone as far as possible, move the gate to new location op_list.insert(new_location + 1, current_gate) op_list.pop(current_location) current_location -= 1 return op_list