def unitary_to_rot(tape): """Quantum function transform to decomposes all instances of single-qubit :class:`~.QubitUnitary` operations to parametrized single-qubit operations. Diagonal operations will be converted to a single :class:`.RZ` gate, while non-diagonal operations will be converted to a :class:`.Rot` gate that implements the original operation up to a global phase. Args: qfunc (function): a quantum function **Example** Suppose we would like to apply the following unitary operation: .. code-block:: python3 U = np.array([ [-0.17111489+0.58564875j, -0.69352236-0.38309524j], [ 0.25053735+0.75164238j, 0.60700543-0.06171855j] ]) The ``unitary_to_rot`` transform enables us to decompose such numerical operations (as well as unitaries that may be defined by parameters within the QNode, and instantiated therein), while preserving differentiability. .. code-block:: python3 def qfunc(): qml.QubitUnitary(U, wires=0) return qml.expval(qml.PauliZ(0)) The original circuit is: >>> dev = qml.device('default.qubit', wires=1) >>> qnode = qml.QNode(qfunc, dev) >>> print(qml.draw(qnode)()) 0: ──U0──┤ ⟨Z⟩ U0 = [[-0.17111489+0.58564875j -0.69352236-0.38309524j] [ 0.25053735+0.75164238j 0.60700543-0.06171855j]] We can use the transform to decompose the gate: >>> transformed_qfunc = unitary_to_rot(qfunc) >>> transformed_qnode = qml.QNode(transformed_qfunc, dev) >>> print(qml.draw(transformed_qnode)()) 0: ──Rot(-1.35, 1.83, -0.606)──┤ ⟨Z⟩ """ for op in tape.operations + tape.measurements: if isinstance(op, qml.QubitUnitary): # Only act on single-qubit unitary operations if qml.math.shape(op.parameters[0]) != (2, 2): continue zyz_decomposition(op.parameters[0], op.wires[0]) else: qml.apply(op)
def test_gradients_gaussian_circuit(self, op, obs, tol): """Tests that the gradients of circuits of gaussian gates match between the finite difference and analytic methods.""" tol = 1e-2 with qml.tape.JacobianTape() as tape: qml.Displacement(0.5, 0, wires=0) qml.apply(op) qml.Beamsplitter(1.3, -2.3, wires=[0, 1]) qml.Displacement(-0.5, 0.1, wires=0) qml.Squeezing(0.5, -1.5, wires=0) qml.Rotation(-1.1, wires=0) qml.expval(obs(wires=0)) dev = qml.device("default.gaussian", wires=2) res = tape.execute(dev) tape.trainable_params = set(range(2, 2 + op.num_params)) tapes, fn = qml.gradients.finite_diff(tape) grad_F = fn(dev.batch_execute(tapes)) tapes, fn = param_shift_cv(tape, dev, force_order2=True) grad_A2 = fn(dev.batch_execute(tapes)) # check that every parameter is analytic for i in range(op.num_params): assert tape._par_info[2 + i]["grad_method"][0] == "A" assert np.allclose(grad_A2, grad_F, atol=tol, rtol=0) if obs.ev_order == 1: tapes, fn = param_shift_cv(tape, dev) grad_A = fn(dev.batch_execute(tapes)) assert np.allclose(grad_A, grad_F, atol=tol, rtol=0)
def test_gradients_gaussian_circuit(self, op, obs, mocker, tol): """Tests that the gradients of circuits of gaussian gates match between the finite difference and analytic methods.""" tol = 1e-2 args = np.linspace(0.2, 0.5, op.num_params) with CVParamShiftTape() as tape: qml.Displacement(0.5, 0, wires=0) qml.apply(op) qml.Beamsplitter(1.3, -2.3, wires=[0, 1]) qml.Displacement(-0.5, 0.1, wires=0) qml.Squeezing(0.5, -1.5, wires=0) qml.Rotation(-1.1, wires=0) qml.var(obs(wires=0)) dev = qml.device("default.gaussian", wires=2) res = tape.execute(dev) tape._update_gradient_info() tape.trainable_params = set(range(2, 2 + op.num_params)) # check that every parameter is analytic for i in range(op.num_params): assert tape._par_info[2 + i]["grad_method"][0] == "A" spy = mocker.spy(CVParamShiftTape, "parameter_shift_first_order") grad_F = tape.jacobian(dev, method="numeric") grad_A = tape.jacobian(dev, method="analytic") grad_A2 = tape.jacobian(dev, method="analytic", force_order2=True) assert np.allclose(grad_A2, grad_F, atol=tol, rtol=0) assert np.allclose(grad_A, grad_F, atol=tol, rtol=0)
def test_nested_apply(self, qnodes, interface, tf_support, torch_support, tol): """Test that nested apply can be done using all interfaces""" if interface == "torch" and not torch_support: pytest.skip("Skipped, no torch support") if interface == "tf" and not tf_support: pytest.skip("Skipped, no tf support") qnode1, qnode2 = qnodes qc = qml.QNodeCollection([qnode1, qnode2]) if interface == "tf": sinfn = tf.sin sfn = tf.reduce_sum elif interface == "torch": sinfn = torch.sin sfn = torch.sum else: sinfn = np.sin sfn = np.sum cost = qml.apply(sfn, qml.apply(sinfn, qc)) params = [0.5643, -0.45] res = cost(params) expected = sfn(sinfn(qc(params))) assert np.allclose(res, expected, atol=tol, rtol=0)
def test_control_gates(self, op, label): """Test a variety of non-special gates. Checks control wires are drawn, and that a box is drawn over the target wires.""" with QuantumTape() as tape: qml.apply(op) _, ax = tape_mpl(tape) layer = 0 assert isinstance(ax.patches[0], mpl.patches.Circle) assert ax.patches[0].center == (layer, 0) control_line = ax.lines[2] assert control_line.get_data() == ((layer, layer), (0, 1)) assert isinstance(ax.patches[1], mpl.patches.FancyBboxPatch) assert ax.patches[1].get_x() == layer - self.width / 2.0 assert ax.patches[1].get_y() == 1 - self.width / 2.0 # two wire labels, so [2] is box gate label assert ax.texts[2].get_text() == label # box and text must be raised above control wire # text raised over box assert ax.patches[1].get_zorder() > control_line.get_zorder() assert ax.texts[2].get_zorder() > ax.patches[1].get_zorder() plt.close()
def my_transform(tape, weights): """Generates two tapes, one with all RX replaced with RY, and the other with all RX replaced with RZ.""" tape1 = qml.tape.JacobianTape() tape2 = qml.tape.JacobianTape() # loop through all operations on the input tape for op in tape.operations + tape.measurements: if op.name == "RX": wires = op.wires param = op.parameters[0] with tape1: qml.RY(weights[0] * qml.math.sin(param), wires=wires) with tape2: qml.RZ(weights[1] * qml.math.cos(param), wires=wires) else: for t in [tape1, tape2]: with t: qml.apply(op) def processing_fn(results): return qml.math.sum(qml.math.stack(results)) return [tape1, tape2], processing_fn
def test_apply(self, op, apply_unitary, tol): """Test the application of gates to a state""" dev = plf.NumpyWavefunctionDevice(wires=3) obs = qml.expval(qml.PauliZ(0)) if op.name == "QubitUnitary": state = apply_unitary(U, 3) elif op.name == "BasisState": state = np.array([0, 0, 0, 0, 0, 0, 0, 1]) elif op.name == "CPHASE": state = apply_unitary(test_operation_map["CPHASE"](0.432, 2), 3) elif op.name == "ISWAP": state = apply_unitary(test_operation_map["ISWAP"], 3) elif op.name == "PSWAP": state = apply_unitary(test_operation_map["PSWAP"](0.432), 3) else: state = apply_unitary(op.matrix, 3) with qml.tape.QuantumTape() as tape: qml.apply(op) obs dev.apply(tape.operations, rotations=tape.diagonalizing_gates) # verify the device is now in the expected state self.assertAllAlmostEqual(dev._state, state, delta=tol)
def test_gradients(self, op, obs, tol, dev): """Tests that the gradients of circuits match between the finite difference and device methods.""" with qml.tape.JacobianTape() as tape: qml.Hadamard(wires=0) qml.RX(0.543, wires=0) qml.CNOT(wires=[0, 1]) qml.apply(op) qml.Rot(1.3, -2.3, 0.5, wires=[0]) qml.RZ(-0.5, wires=0) qml.RY(0.5, wires=1).inv() qml.CNOT(wires=[0, 1]) qml.expval(obs(wires=0)) qml.expval(qml.PauliZ(wires=1)) tape.trainable_params = set(range(1, 1 + op.num_params)) grad_F = (lambda t, fn: fn(qml.execute(t, dev, None)))( *qml.gradients.finite_diff(tape)) grad_D = dev.adjoint_jacobian(tape) assert np.allclose(grad_D, grad_F, atol=tol, rtol=0)
def circuit(): """4-qubit circuit with layers of randomly selected gates and random connections for multi-qubit gates.""" np.random.seed(1967) for gates in gates_per_layers: for gate in gates: qml.apply(gate) return qml.expval(qml.PauliZ(0))
def test_default_queue_measurements_outside(self, obs): """Test applying a measurement instantiated outside a queuing context to an existing active queue""" op = qml.expval(obs) with qml.tape.QuantumTape() as tape: qml.apply(op) assert tape.measurements == [op]
def test_default_queue_operation_outside(self): """Test applying an operation instantiated outside a queuing context to an existing active queue""" op = qml.PauliZ(0) with qml.tape.QuantumTape() as tape: qml.apply(op) assert tape.operations == [op]
def remove_barrier(tape): """Quantum function transform to remove Barrier gates. 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): qml.Hadamard(wires=0) qml.Hadamard(wires=1) qml.Barrier(wires=[0,1]) qml.PauliX(wires=0) return qml.expval(qml.PauliZ(0)) The circuit before optimization: >>> dev = qml.device('default.qubit', wires=2) >>> qnode = qml.QNode(qfunc, dev) >>> print(qml.draw(qnode)(1, 2)) 0: ──H──╭||──X──┤ ⟨Z⟩ 1: ──H──╰||─────┤ We can remove the Barrier by running the ``remove_barrier`` transform: >>> optimized_qfunc = remove_barrier(qfunc) >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) >>> print(qml.draw(optimized_qnode)(1, 2)) 0: ──H──X──┤ ⟨Z⟩ 1: ──H─────┤ """ # Make a working copy of the list to traverse list_copy = tape.operations.copy() while len(list_copy) > 0: current_gate = list_copy[0] # Remove Barrier gate if current_gate.name != "Barrier": apply(current_gate) list_copy.pop(0) continue # Queue the measurements normally for m in tape.measurements: apply(m)
def test_different_queue_operation_outside(self): """Test applying an operation instantiated outside a queuing context to a specfied queuing context""" op = qml.PauliZ(0) with qml.tape.QuantumTape() as tape1: with qml.tape.QuantumTape() as tape2: qml.apply(op, tape1) assert tape1.operations == [tape2, op] assert tape2.operations == []
def test_different_queue_measurements_outside(self, obs): """Test applying a measurement instantiated outside a queuing context to a specfied queuing context""" op = qml.expval(obs) with qml.tape.QuantumTape() as tape1: with qml.tape.QuantumTape() as tape2: qml.apply(op, tape1) assert tape1.measurements == [op] assert tape2.measurements == []
def test_general_operations_decimals(self, op): """Check that the decimals argument affects text strings when applicable.""" with QuantumTape() as tape: qml.apply(op) _, ax = tape_mpl(tape, decimals=2) num_wires = len(op.wires) assert ax.texts[num_wires].get_text() == op.label(decimals=2) plt.close()
def _get_first_term_tapes(tape, layer_i, layer_j, allow_nonunitary, aux_wire): r"""Obtain the tapes for the first term of all tensor entries belonging to an off-diagonal block. Args: tape (pennylane.tape.QuantumTape): Tape that is being transformed layer_i (list): The first layer of parametrized ops, of the format of the layers generated by ``iterate_parametrized_layers`` layer_j (list): The second layer of parametrized ops allow_nonunitary (bool): Whether non-unitary operations are allowed in the circuit; passed to ``_get_gen_op`` aux_wire (object or pennylane.wires.Wires): Auxiliary wire on which to control the controlled-generator operations Returns: list[pennylane.tape.QuantumTape]: Transformed tapes that compute the first term of the metric tensor for the off-diagonal block belonging to the input layers list[tuple[int]]: 2-tuple indices assigning the tapes to metric tensor entries """ tapes = [] ids = [] # Exclude the backwards cone of layer_i from the backwards cone of layer_j ops_between_cgens = [ op for op in layer_j.pre_ops if op not in layer_i.pre_ops ] # Iterate over differentiated operation in first layer for diffed_op_i, par_idx_i in zip(layer_i.ops, layer_i.param_inds): gen_op_i = _get_gen_op(diffed_op_i, allow_nonunitary, aux_wire) # Iterate over differentiated operation in second layer # There will be one tape per pair of differentiated operations for diffed_op_j, par_idx_j in zip(layer_j.ops, layer_j.param_inds): gen_op_j = _get_gen_op(diffed_op_j, allow_nonunitary, aux_wire) with tape.__class__() as new_tape: # Initialize auxiliary wire qml.Hadamard(wires=aux_wire) # Apply backward cone of first layer for op in layer_i.pre_ops: qml.apply(op) # Controlled-generator operation of first diff'ed op qml.apply(gen_op_i) # Apply first layer and operations between layers for op in ops_between_cgens: qml.apply(op) # Controlled-generator operation of second diff'ed op qml.apply(gen_op_j) # Measure X on auxiliary wire qml.expval(qml.PauliX(aux_wire)) tapes.append(new_tape) # Memorize to which metric entry this tape belongs ids.append((par_idx_i, par_idx_j)) return tapes, ids
def test_apply_no_queue_method(self): """Test that an object with no queue method is still added to the queuing context""" with qml.tape.QuantumTape() as tape1: with qml.tape.QuantumTape() as tape2: op1 = qml.apply(5) op2 = qml.apply(6, tape1) assert tape1.queue == [tape2, op2] assert tape2.queue == [op1] # note that tapes don't know how to process integers, # so they are not included after queue processing assert tape1.operations == [tape2] assert tape2.operations == []
def test_two_qubit_decomposition_tf(self, U, wires): """Test that a two-qubit operation in Tensorflow is correctly decomposed.""" tf = pytest.importorskip("tensorflow") U = tf.Variable(U, dtype=tf.complex128) obtained_decomposition = two_qubit_decomposition(U, wires=wires) with qml.tape.QuantumTape() as tape: for op in obtained_decomposition: qml.apply(op) obtained_matrix = get_unitary_matrix(tape, wire_order=wires)() assert check_matrix_equivalence(U, obtained_matrix, atol=1e-7)
def test_two_qubit_decomposition_1_cnot(self, U, wires): """Test that a two-qubit matrix using one CNOT is correctly decomposed.""" U = _convert_to_su4(np.array(U)) assert _compute_num_cnots(U) == 1 obtained_decomposition = two_qubit_decomposition(U, wires=wires) assert len(obtained_decomposition) == 5 with qml.tape.QuantumTape() as tape: for op in obtained_decomposition: qml.apply(op) obtained_matrix = get_unitary_matrix(tape, wire_order=wires)() assert check_matrix_equivalence(U, obtained_matrix, atol=1e-7)
def append_time_evolution(tape, riemannian_gradient, t, n, exact=False): r"""Append an approximate time evolution, corresponding to a Riemannian gradient on the Lie group, to an existing circuit. We want to implement the time evolution generated by an operator of the form .. math:: \text{grad} f(U) = sum_i c_i O_i, where :math:`O_i` are Pauli words and :math:`c_t \in \mathbb{R}`. If ``exact`` is ``False``, we Trotterize this operator and apply the unitary .. math:: U' = \prod_{n=1}^{N_{Trot.}} \left(\prod_i \exp{-it / N_{Trot.} O_i}\right), which is then appended to the current circuit. If ``exact`` is ``True``, we calculate the exact time evolution for the Riemannian gradient by way of the matrix exponential. .. math: U' = \exp{-it \text{grad} f(U)} and append this unitary. Args: tape (QuantumTape or .QNode): circuit to transform. riemannian_gradient (.Hamiltonian): Hamiltonian object representing the Riemannian gradient. t (float): time evolution parameter. n (int): number of Trotter steps. """ for obj in tape.operations: qml.apply(obj) if exact: qml.QubitUnitary( expm(-1j * t * qml.utils.sparse_hamiltonian(riemannian_gradient).toarray()), wires=range(len(riemannian_gradient.wires)), ) else: qml.templates.ApproxTimeEvolution(riemannian_gradient, t, n) for obj in tape.measurements: qml.apply(obj)
def algebra_commutator(tape, observables, lie_algebra_basis_names, nqubits): """Calculate the Riemannian gradient in the Lie algebra with the parameter shift rule (see :meth:`LieAlgebraOptimizer.get_omegas`). Args: tape (.QuantumTape or .QNode): input circuit observables (list[.Observable]): list of observables to be measured. Can be grouped. lie_algebra_basis_names (list[str]): List of strings corresponding to valid Pauli words. nqubits (int): the number of qubits. Returns: func: Function which accepts the same arguments as the QNode. When called, this function will return the Lie algebra commutator. """ tapes_plus_total = [] tapes_min_total = [] for obs in observables: for o in obs: # create a list of tapes for the plus and minus shifted circuits tapes_plus = [qml.tape.JacobianTape(p + "_p") for p in lie_algebra_basis_names] tapes_min = [qml.tape.JacobianTape(p + "_m") for p in lie_algebra_basis_names] # loop through all operations on the input tape for op in tape.operations: for t in tapes_plus + tapes_min: with t: qml.apply(op) for i, t in enumerate(tapes_plus): with t: qml.PauliRot( np.pi / 2, lie_algebra_basis_names[i], wires=list(range(nqubits)), ) qml.expval(o) for i, t in enumerate(tapes_min): with t: qml.PauliRot( -np.pi / 2, lie_algebra_basis_names[i], wires=list(range(nqubits)), ) qml.expval(o) tapes_plus_total.extend(tapes_plus) tapes_min_total.extend(tapes_min) return tapes_plus_total + tapes_min_total, None
def test_two_qubit_decomposition_tensor_products(self, U_pair, wires): """Test that a two-qubit tensor product matrix is correctly decomposed.""" U = _convert_to_su4( qml.math.kron(np.array(U_pair[0]), np.array(U_pair[1]))) assert _compute_num_cnots(U) == 0 obtained_decomposition = two_qubit_decomposition(U, wires=wires) assert len(obtained_decomposition) == 2 with qml.tape.QuantumTape() as tape: for op in obtained_decomposition: qml.apply(op) obtained_matrix = get_unitary_matrix(tape, wire_order=wires)() assert check_matrix_equivalence(U, obtained_matrix, atol=1e-7)
def test_default_queue_operation_inside(self): """Test applying an operation instantiated within the queuing context to the existing active queue""" with qml.tape.QuantumTape() as tape: op1 = qml.PauliZ(0) op2 = qml.apply(op1) assert tape.operations == [op1, op2]
def test_general_operations(self, op): """Test that a variety of operations produce a rectangle across relevant wires and a correct label text.""" with QuantumTape() as tape: qml.apply(op) _, ax = tape_mpl(tape) num_wires = len(op.wires) assert ax.texts[num_wires].get_text() == op.label() assert isinstance(ax.patches[0], mpl.patches.Rectangle) assert ax.patches[0].get_xy() == (-0.4, -0.4) assert ax.patches[0].get_width() == 0.8 assert ax.patches[0].get_height() == num_wires - 0.2 plt.close()
def test_measurements(self, measurements, wires): """Tests a variety of measurements draw measurement boxes on the correct wires.""" with QuantumTape() as tape: for m in measurements: qml.apply(m) _, ax = tape_mpl(tape) assert len(ax.patches) == 3 * len(wires) for ii, w in enumerate(wires): assert ax.patches[3 * ii].get_xy() == (0.6, w - 0.4) # rectangle assert ax.patches[3 * ii + 1].center == (1, w + 0.05) # arc assert isinstance(ax.patches[3 * ii + 2], mpl.patches.FancyArrow) # fancy arrow plt.close()
def test_two_qubit_decomposition_tensor_products_torch( self, U_pair, wires): """Test that a two-qubit tensor product in Torch is correctly decomposed.""" torch = pytest.importorskip("torch") U1 = torch.tensor(U_pair[0], dtype=torch.complex128) U2 = torch.tensor(U_pair[1], dtype=torch.complex128) U = qml.math.kron(U1, U2) obtained_decomposition = two_qubit_decomposition(U, wires=wires) with qml.tape.QuantumTape() as tape: for op in obtained_decomposition: qml.apply(op) obtained_matrix = get_unitary_matrix(tape, wire_order=wires)() assert check_matrix_equivalence(U, obtained_matrix, atol=1e-7)
def test_two_qubit_decomposition_3_cnots(self, U, wires): """Test that a two-qubit matrix using 3 CNOTs is correctly decomposed.""" U = _convert_to_su4(np.array(U)) assert _compute_num_cnots(U) == 3 obtained_decomposition = two_qubit_decomposition(U, wires=wires) assert len(obtained_decomposition) == 10 with qml.tape.QuantumTape() as tape: for op in obtained_decomposition: qml.apply(op) obtained_matrix = get_unitary_matrix(tape, wire_order=wires)() # We check with a slightly great tolerance threshold here simply because the # test matrices were copied in here with reduced precision. assert check_matrix_equivalence(U, obtained_matrix, atol=1e-7)
def test_different_queue_operation_inside(self): """Test applying an operation instantiated within the queuing context to a specfied queuing context""" with qml.tape.QuantumTape() as tape1: with qml.tape.QuantumTape() as tape2: op1 = qml.PauliZ(0) op2 = qml.apply(op1, tape1) assert tape1.operations == [tape2, op2] assert tape2.operations == [op1]
def test_two_qubit_decomposition_jax(self, U, wires): """Test that a two-qubit operation in JAX is correctly decomposed.""" jax = pytest.importorskip("jax") from jax.config import config remember = config.read("jax_enable_x64") config.update("jax_enable_x64", True) U = jax.numpy.array(U, dtype=jax.numpy.complex128) obtained_decomposition = two_qubit_decomposition(U, wires=wires) with qml.tape.QuantumTape() as tape: for op in obtained_decomposition: qml.apply(op) obtained_matrix = get_unitary_matrix(tape, wire_order=wires)() assert check_matrix_equivalence(U, obtained_matrix, atol=1e-7)
def test_compare_analytic_and_numeric_gradients(self, op, mocker, tol): """Test for selected gates that the gradients of circuits match between the finite difference and analytic methods.""" with ReversibleTape() as tape: qml.Hadamard(wires=0) qml.RX(0.543, wires=0) qml.CNOT(wires=[0, 1]) qml.apply(op) qml.Rot(1.3, -2.3, 0.5, wires=[0]) qml.RZ(-0.5, wires=0) qml.RY(0.5, wires=1) qml.CNOT(wires=[0, 1]) qml.expval(qml.PauliX(wires=0)) qml.expval(qml.PauliZ(wires=1)) dev = qml.device("default.qubit", wires=2) res = tape.execute(dev) tape._update_gradient_info() tape.trainable_params = set(range(1, 1 + op.num_params)) # check that every parameter is analytic for i in range(op.num_params): assert tape._par_info[1 + i]["grad_method"][0] == "A" grad_F = tape.jacobian(dev, method="numeric") spy = mocker.spy(ReversibleTape, "analytic_pd") spy_execute = mocker.spy(tape, "execute_device") grad_A = tape.jacobian(dev, method="analytic") spy.assert_called() # check that the execute device method has only been called # once, for all parameters. spy_execute.assert_called_once() assert np.allclose(grad_A, grad_F, atol=tol, rtol=0)