Esempio n. 1
0
    def test_rot_decomposition(self, diff_method):
        """Test that the rotation gate is correctly decomposed"""
        dev = qml.device("default.qubit", wires=1)

        def circuit(weights):
            qml.Rot(weights[0], weights[1], weights[2], wires=0)
            return qml.expval(qml.PauliX(0))

        circuit = qml.QNode(circuit, dev, diff_method=diff_method)
        params = np.array([1., 2., 3.])
        tapes = qml.metric_tensor(circuit, only_construct=True)(params)
        assert len(tapes) == 3

        # first parameter subcircuit
        assert len(tapes[0].operations) == 0

        # Second parameter subcircuit
        assert len(tapes[1].operations) == 4
        assert isinstance(tapes[1].operations[0], qml.RZ)
        assert tapes[1].operations[0].data == [1]
        # PauliY decomp
        assert isinstance(tapes[1].operations[1], qml.PauliZ)
        assert isinstance(tapes[1].operations[2], qml.S)
        assert isinstance(tapes[1].operations[3], qml.Hadamard)

        # Third parameter subcircuit
        assert len(tapes[2].operations) == 2
        assert isinstance(tapes[2].operations[0], qml.RZ)
        assert isinstance(tapes[2].operations[1], qml.RY)
        assert tapes[2].operations[0].data == [1]
        assert tapes[2].operations[1].data == [2]

        result = qml.metric_tensor(circuit)(params)
        assert result.shape == (3, 3)
Esempio n. 2
0
    def test_multiple_devices(self, mocker):
        """Test that passing multiple devices to ExpvalCost works correctly"""

        dev = [
            qml.device("default.qubit", wires=2),
            qml.device("default.mixed", wires=2)
        ]
        spy = mocker.spy(DefaultQubit, "apply")
        spy2 = mocker.spy(DefaultMixed, "apply")

        obs = [qml.PauliZ(0), qml.PauliZ(1)]
        h = qml.Hamiltonian([1, 1], obs)

        qnodes = qml.ExpvalCost(qml.templates.BasicEntanglerLayers, h, dev)
        np.random.seed(1967)
        w = np.random.random(
            qml.templates.BasicEntanglerLayers.shape(n_layers=3, n_wires=2))

        res = qnodes(w)

        spy.assert_called_once()
        spy2.assert_called_once()

        mapped = qml.map(qml.templates.BasicEntanglerLayers, obs, dev)
        exp = sum(mapped(w))

        assert np.allclose(res, exp)

        with pytest.warns(
                UserWarning,
                match="ExpvalCost was instantiated with multiple devices."):
            qml.metric_tensor(qnodes)(w)
    def test_generator_no_expval(self, monkeypatch):
        """Test exception is raised if subcircuit contains an
        operation with generator object that is not an observable"""
        with monkeypatch.context() as m:
            m.setattr("pennylane.RX.generator", [qml.RX, 1])

            with qml.tape.QuantumTape() as tape:
                qml.RX(np.array(0.5, requires_grad=True), wires=0)
                qml.expval(qml.PauliX(0))

            with pytest.raises(qml.QuantumFunctionError, match="no corresponding observable"):
                qml.metric_tensor(tape, approx="block-diag")
    def test_construct_subcircuit(self):
        """Test correct subcircuits constructed"""
        dev = qml.device("default.qubit", wires=2)

        with qml.tape.QuantumTape() as tape:
            qml.RX(np.array(1.0, requires_grad=True), wires=0)
            qml.RY(np.array(1.0, requires_grad=True), wires=0)
            qml.CNOT(wires=[0, 1])
            qml.PhaseShift(np.array(1.0, requires_grad=True), wires=1)
            return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1))

        tapes, _ = qml.metric_tensor(tape, approx="block-diag")
        assert len(tapes) == 3

        # first parameter subcircuit
        assert len(tapes[0].operations) == 1
        assert isinstance(tapes[0].operations[0], qml.Hadamard)  # PauliX decomp

        # second parameter subcircuit
        assert len(tapes[1].operations) == 4
        assert isinstance(tapes[1].operations[0], qml.RX)
        # PauliY decomp
        assert isinstance(tapes[1].operations[1], qml.PauliZ)
        assert isinstance(tapes[1].operations[2], qml.S)
        assert isinstance(tapes[1].operations[3], qml.Hadamard)

        # third parameter subcircuit
        assert len(tapes[2].operations) == 4
        assert isinstance(tapes[2].operations[0], qml.RX)
        assert isinstance(tapes[2].operations[1], qml.RY)
        assert isinstance(tapes[2].operations[2], qml.CNOT)
        # Phase shift generator
        assert isinstance(tapes[2].operations[3], qml.QubitUnitary)
Esempio n. 5
0
    def test_multi_qubit_gates(self):
        """Test that a tape with Ising gates has the correct metric tensor tapes."""

        dev = qml.device("default.qubit", wires=3)
        with qml.tape.JacobianTape() as tape:
            qml.Hadamard(0)
            qml.Hadamard(2)
            qml.IsingXX(0.2, wires=[0, 1])
            qml.IsingXX(-0.6, wires=[1, 2])
            qml.IsingZZ(1.02, wires=[0, 1])
            qml.IsingZZ(-4.2, wires=[1, 2])

        tapes, proc_fn = qml.metric_tensor(tape, approx="block-diag")
        assert len(tapes) == 4
        assert [len(tape.operations) for tape in tapes] == [2, 4, 5, 6]
        assert [len(tape.measurements) for tape in tapes] == [1] * 4
        expected_ops = [
            [qml.Hadamard, qml.QubitUnitary],
            [qml.Hadamard, qml.Hadamard, qml.IsingXX, qml.QubitUnitary],
            [
                qml.Hadamard, qml.Hadamard, qml.IsingXX, qml.IsingXX,
                qml.QubitUnitary
            ],
            [
                qml.Hadamard, qml.Hadamard, qml.IsingXX, qml.IsingXX,
                qml.IsingZZ, qml.QubitUnitary
            ],
        ]
        assert [[type(op) for op in tape.operations]
                for tape in tapes] == expected_ops
    def test_evaluate_diag_metric_tensor(self, tol):
        """Test that a diagonal metric tensor evaluates correctly"""
        dev = qml.device("default.qubit", wires=2)

        def circuit(a, b, c):
            qml.RX(a, wires=0)
            qml.RY(b, wires=0)
            qml.CNOT(wires=[0, 1])
            qml.PhaseShift(c, wires=1)
            return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1))

        circuit = qml.QNode(circuit, dev)

        a = 0.432
        b = 0.12
        c = -0.432

        # evaluate metric tensor
        g = qml.metric_tensor(circuit, approx="block-diag")(a, b, c)

        # check that the metric tensor is correct
        expected = (
            np.array(
                [1, np.cos(a) ** 2, (3 - 2 * np.cos(a) ** 2 * np.cos(2 * b) - np.cos(2 * a)) / 4]
            )
            / 4
        )
        assert np.allclose(g, np.diag(expected), atol=tol, rtol=0)
    def test_metric_tensor_tape_mode(self):
        """Test that the metric tensor can be calculated in tape mode, and that it is equal to a
        metric tensor calculated in non-tape mode."""
        if not qml.tape_mode_active():
            pytest.skip("This test is only intended for tape mode")

        dev = qml.device("default.qubit", wires=2)
        p = np.array([1., 1., 1.])

        def ansatz(params, **kwargs):
            qml.RX(params[0], wires=0)
            qml.RY(params[1], wires=0)
            qml.CNOT(wires=[0, 1])
            qml.PhaseShift(params[2], wires=1)

        h = qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(1)])
        qnodes = qml.ExpvalCost(ansatz, h, dev)
        mt = qml.metric_tensor(qnodes)(p)
        assert qml.tape_mode_active()  # Check that tape mode is still active

        qml.disable_tape()

        @qml.qnode(dev)
        def circuit(params):
            qml.RX(params[0], wires=0)
            qml.RY(params[1], wires=0)
            qml.CNOT(wires=[0, 1])
            qml.PhaseShift(params[2], wires=1)
            return qml.expval(qml.PauliZ(0))

        mt2 = circuit.metric_tensor([p])
        assert np.allclose(mt, mt2)
Esempio n. 8
0
    def test_torch(self, diff_method, tol):
        """Test metric tensor differentiability in the torch interface"""
        if diff_method == "backprop":
            pytest.skip("Does not support backprop")

        torch = pytest.importorskip("torch")

        dev = qml.device("default.qubit", wires=2)

        @qml.qnode(dev, interface="torch", diff_method=diff_method)
        def circuit(weights):
            qml.RX(weights[0], wires=0)
            qml.RY(weights[1], wires=0)
            qml.CNOT(wires=[0, 1])
            qml.PhaseShift(weights[2], wires=1)
            return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1))

        weights = np.array([0.432, 0.12, -0.432])
        a, b, c = weights

        weights_t = torch.tensor(weights, requires_grad=True)
        loss = qml.metric_tensor(circuit)(weights_t)[2, 2]
        loss.backward()

        grad = weights_t.grad
        expected = np.array([
            np.cos(a) * np.cos(b)**2 * np.sin(a) / 2,
            np.cos(a)**2 * np.sin(2 * b) / 4, 0
        ])
        assert np.allclose(grad, expected, atol=tol, rtol=0)
    def test_rot_decomposition(self, diff_method):
        """Test that the rotation gate is correctly decomposed"""
        dev = qml.device("default.qubit", wires=1)
        params = np.array([1.0, 2.0, 3.0], requires_grad=True)

        with qml.tape.QuantumTape() as circuit:
            qml.Rot(params[0], params[1], params[2], wires=0)
            qml.expval(qml.PauliX(0))

        tapes, _ = qml.metric_tensor(circuit, approx="block-diag")
        assert len(tapes) == 3

        # first parameter subcircuit
        assert len(tapes[0].operations) == 0

        # Second parameter subcircuit
        assert len(tapes[1].operations) == 4
        assert isinstance(tapes[1].operations[0], qml.RZ)
        assert tapes[1].operations[0].data == [1]
        # PauliY decomp
        assert isinstance(tapes[1].operations[1], qml.PauliZ)
        assert isinstance(tapes[1].operations[2], qml.S)
        assert isinstance(tapes[1].operations[3], qml.Hadamard)

        # Third parameter subcircuit
        assert len(tapes[2].operations) == 2
        assert isinstance(tapes[2].operations[0], qml.RZ)
        assert isinstance(tapes[2].operations[1], qml.RY)
        assert tapes[2].operations[0].data == [1]
        assert tapes[2].operations[1].data == [2]
Esempio n. 10
0
    def test_tf(self, diff_method, tol):
        """Test metric tensor differentiability in the TF interface"""
        tf = pytest.importorskip("tensorflow", minversion="2.0")

        dev = qml.device("default.qubit", wires=2)

        @qml.qnode(dev, interface="tf", diff_method=diff_method)
        def circuit(weights):
            qml.RX(weights[0], wires=0)
            qml.RY(weights[1], wires=0)
            qml.CNOT(wires=[0, 1])
            qml.PhaseShift(weights[2], wires=1)
            return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1))

        weights = np.array([0.432, 0.12, -0.432])
        weights_t = tf.Variable(weights)
        a, b, c = weights

        with tf.GradientTape() as tape:
            loss = qml.metric_tensor(circuit)(weights_t)[2, 2]

        grad = tape.gradient(loss, weights_t)
        expected = np.array([
            np.cos(a) * np.cos(b)**2 * np.sin(a) / 2,
            np.cos(a)**2 * np.sin(2 * b) / 4, 0
        ])
        assert np.allclose(grad, expected, atol=tol, rtol=0)
    def test_evaluate_diag_metric_tensor_classical_processing(self, tol):
        """Test that a diagonal metric tensor evaluates correctly
        when the QNode includes classical processing."""
        dev = qml.device("default.qubit", wires=2)

        def circuit(a, b):
            # The classical processing function is
            #     f: ([a0, a1], b) -> (a1, a0, b)
            # So the classical Jacobians will be a permutation matrix and an identity matrix:
            #     classical_jacobian(circuit)(a, b) == ([[0, 1], [1, 0]], [[1]])
            qml.RX(a[1], wires=0)
            qml.RY(a[0], wires=0)
            qml.CNOT(wires=[0, 1])
            qml.PhaseShift(b, wires=1)
            return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1))

        circuit = qml.QNode(circuit, dev)

        a = np.array([0.432, 0.1])
        b = 0.12

        # evaluate metric tensor
        g = qml.metric_tensor(circuit, approx="block-diag")(a, b)
        assert isinstance(g, tuple)
        assert len(g) == 2
        assert g[0].shape == (len(a), len(a))
        assert g[1].shape == tuple()

        # check that the metric tensor is correct
        expected = np.array([np.cos(a[1]) ** 2, 1]) / 4
        assert np.allclose(g[0], np.diag(expected), atol=tol, rtol=0)

        expected = (3 - 2 * np.cos(a[1]) ** 2 * np.cos(2 * a[0]) - np.cos(2 * a[1])) / 16
        assert np.allclose(g[1], expected, atol=tol, rtol=0)
Esempio n. 12
0
    def step_and_cost(self,
                      qnode,
                      *args,
                      grad_fn=None,
                      recompute_tensor=True,
                      metric_tensor_fn=None,
                      **kwargs):
        """Update the parameter array :math:`x` with one step of the optimizer and return the
        corresponding objective function value prior to the step.

        Args:
            qnode (QNode): the QNode for optimization
            *args : variable length argument list for qnode
            grad_fn (function): optional gradient function of the
                qnode with respect to the variables ``*args``.
                If ``None``, the gradient function is computed automatically.
                Must return a ``tuple[array]`` with the same number of elements as ``*args``.
                Each array of the tuple should have the same shape as the corresponding argument.
            recompute_tensor (bool): Whether or not the metric tensor should
                be recomputed. If not, the metric tensor from the previous
                optimization step is used.
            metric_tensor_fn (function): Optional metric tensor function
                with respect to the variables ``args``.
                If ``None``, the metric tensor function is computed automatically.
            **kwargs : variable length of keyword arguments for the qnode

        Returns:
            tuple: the new variable values :math:`x^{(t+1)}` and the objective function output
            prior to the step
        """
        # pylint: disable=arguments-differ
        if not isinstance(
                qnode,
            (qml.QNode, qml.ExpvalCost)) and metric_tensor_fn is None:
            raise ValueError(
                "The objective function must either be encoded as a single QNode or "
                "an ExpvalCost object for the natural gradient to be automatically computed. "
                "Otherwise, metric_tensor_fn must be explicitly provided to the optimizer."
            )

        if recompute_tensor or self.metric_tensor is None:
            if metric_tensor_fn is None:
                metric_tensor_fn = qml.metric_tensor(
                    qnode, diag_approx=self.diag_approx)

            self.metric_tensor = metric_tensor_fn(*args, **kwargs)
            self.metric_tensor += self.lam * np.identity(
                self.metric_tensor.shape[0])

        g, forward = self.compute_grad(qnode, args, kwargs, grad_fn=grad_fn)
        new_args = self.apply_grad(g, args)

        if forward is None:
            forward = qnode(*args, **kwargs)

        # unwrap from list if one argument, cleaner return
        if len(new_args) == 1:
            return new_args[0], forward
        return new_args, forward
    def test_template_integration(self, strategy, tol):
        """Test that the metric tensor transform acts on QNodes
        correctly when the QNode contains a template"""
        dev = qml.device("default.qubit", wires=3)

        @qml.beta.qnode(dev, expansion_strategy=strategy)
        def circuit(weights):
            qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1, 2])
            return qml.probs(wires=[0, 1])

        weights = np.ones([2, 3, 3], dtype=np.float64, requires_grad=True)
        res = qml.metric_tensor(circuit, approx="block-diag")(weights)
        assert res.shape == (2, 3, 3, 2, 3, 3)
Esempio n. 14
0
    def test_multirz_decomposition(self, diff_method):
        """Test that the MultiRZ gate is correctly decomposed"""
        dev = qml.device("default.qubit", wires=3)

        def circuit(a, b):
            qml.RX(a, wires=0)
            qml.MultiRZ(b, wires=[0, 1, 2])
            return qml.expval(qml.PauliX(0))

        circuit = qml.QNode(circuit, dev, diff_method=diff_method)
        params = [0.1, 0.2]
        result = qml.metric_tensor(circuit)(*params)
        assert result.shape == (2, 2)
    def test_full_tensor_not_implemented(self):
        """Test that the full metric tensor can not be computed yet."""
        dev = qml.device("default.qubit", wires=3)

        def circuit(a, b):
            qml.RX(a, wires=0)
            qml.MultiRZ(b, wires=[0, 1, 2])
            return qml.expval(qml.PauliX(0))

        circuit = qml.QNode(circuit, dev)
        params = [0.1, 0.2]
        with pytest.raises(NotImplementedError):
            result = qml.metric_tensor(circuit, approx=None)(*params)
Esempio n. 16
0
    def test_single_qubit_vqe(self, tol, approx):
        """Test single-qubit VQE has the correct QNG value
        every step, the correct parameter updates,
        and correct cost after 200 steps"""
        dev = qml.device("default.qubit", wires=(2 if approx is None else 1))

        def circuit(params, wires=None):
            qml.RX(params[0], wires=0)
            qml.RY(params[1], wires=0)

        coeffs = [1, 1]
        obs_list = [qml.PauliX(0), qml.PauliZ(0)]

        qnodes = qml.map(circuit, obs_list, dev, measure="expval")
        cost_fn = qml.dot(coeffs, qnodes)

        def gradient(params):
            """Returns the gradient"""
            da = -np.sin(params[0]) * (np.cos(params[1]) + np.sin(params[1]))
            db = np.cos(params[0]) * (np.cos(params[1]) - np.sin(params[1]))
            return np.array([da, db])

        eta = 0.01
        init_params = np.array([0.011, 0.012])
        num_steps = 200

        opt = qml.QNGOptimizer(eta)
        theta = init_params

        # optimization for 200 steps total
        for t in range(num_steps):
            theta_new = opt.step(cost_fn,
                                 theta,
                                 metric_tensor_fn=qml.metric_tensor(
                                     qnodes.qnodes[0], approx=approx))

            # check metric tensor
            res = opt.metric_tensor
            exp = np.diag([0.25, (np.cos(theta[0])**2) / 4])
            assert np.allclose(res, exp, atol=tol, rtol=0)

            # check parameter update
            dtheta = eta * sp.linalg.pinvh(exp) @ gradient(theta)
            assert np.allclose(dtheta, theta - theta_new, atol=tol, rtol=0)

            theta = theta_new

        # check final cost
        assert np.allclose(cost_fn(theta), -1.41421356, atol=tol, rtol=0)
Esempio n. 17
0
    def metric_tensor(self, *args, diag_approx=False, only_construct=False, **kwargs):
        """Evaluate the value of the metric tensor.

        Args:
            args (tuple[Any]): positional arguments
            kwargs (dict[str, Any]): auxiliary arguments
            diag_approx (bool): iff True, use the diagonal approximation
            only_construct (bool): Iff True, construct the circuits used for computing
                the metric tensor but do not execute them, and return the tapes.

        Returns:
            array[float]: metric tensor
        """
        return qml.metric_tensor(self, diag_approx=diag_approx, only_construct=only_construct)(
            *args, **kwargs
        )
Esempio n. 18
0
    def step_and_cost(self,
                      qnode,
                      x,
                      recompute_tensor=True,
                      metric_tensor_fn=None):
        """Update the parameter array :math:`x` with one step of the optimizer and return the
        corresponding objective function value prior to the step.

        Args:
            qnode (QNode): the QNode for optimization
            x (array): NumPy array containing the current values of the variables to be updated
            recompute_tensor (bool): Whether or not the metric tensor should
                be recomputed. If not, the metric tensor from the previous
                optimization step is used.
            metric_tensor_fn (function): Optional metric tensor function
                with respect to the variables ``x``.
                If ``None``, the metric tensor function is computed automatically.

        Returns:
            tuple: the new variable values :math:`x^{(t+1)}` and the objective function output
            prior to the step
        """
        # pylint: disable=arguments-differ
        if not isinstance(
                qnode,
            (qml.QNode, qml.ExpvalCost)) and metric_tensor_fn is None:
            raise ValueError(
                "The objective function must either be encoded as a single QNode or "
                "an ExpvalCost object for the natural gradient to be automatically computed. "
                "Otherwise, metric_tensor_fn must be explicitly provided to the optimizer."
            )

        if recompute_tensor or self.metric_tensor is None:
            if metric_tensor_fn is None:
                # pseudo-inverse metric tensor
                self.metric_tensor = qml.metric_tensor(
                    qnode, diag_approx=self.diag_approx)(x)
            else:
                self.metric_tensor = metric_tensor_fn(x)
            self.metric_tensor += self.lam * np.identity(
                self.metric_tensor.shape[0])

        # The QNGOptimizer.step does not permit passing an external gradient function.
        # Autograd will always calculate the gradient and `forward` will never be `None`.
        g, forward = self.compute_grad(qnode, (x, ), dict())
        x_out = self.apply_grad(g, x)
        return x_out, forward
Esempio n. 19
0
    def test_parameter_fan_out(self, diff_method):
        """The metric tensor is always with respect to the quantum circuit. Any
        classical processing is not taken into account. As a result, if there is
        parameter fan-out, the returned metric tensor will be larger than
        expected.
        """
        dev = qml.device("default.qubit", wires=2)

        def circuit(a):
            qml.RX(a, wires=0)
            qml.RX(a, wires=0)
            return qml.expval(qml.PauliX(0))

        circuit = qml.QNode(circuit, dev, diff_method=diff_method)
        params = [0.1]
        result = qml.metric_tensor(circuit)(*params)
        assert result.shape == (2, 2)
Esempio n. 20
0
    def test_metric_tensor(self):
        """Test that the metric tensor can be calculated."""

        dev = qml.device("default.qubit", wires=2)
        p = np.array([1.0, 1.0, 1.0])

        def ansatz(params, **kwargs):
            qml.RX(params[0], wires=0)
            qml.RY(params[1], wires=0)
            qml.CNOT(wires=[0, 1])
            qml.PhaseShift(params[2], wires=1)

        h = qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(1)])
        qnodes = qml.ExpvalCost(ansatz, h, dev)
        mt = qml.metric_tensor(qnodes)(p)
        assert mt.shape == (3, 3)
        assert isinstance(mt, np.ndarray)
    def test_evaluate_block_diag_metric_tensor(self, sample_circuit, tol):
        """Test that a block-diagonal metric tensor evaluates correctly,
        by comparing it to a known analytic result as well as numerical
        computation."""
        dev, circuit, non_parametrized_layer, a, b, c = sample_circuit

        params = [-0.282203, 0.145554, 0.331624, -0.163907, 0.57662, 0.081272]
        x, y, z, h, g, f = params

        G = qml.metric_tensor(circuit, approx="block-diag")(*params)

        # ============================================
        # Test block-diag metric tensor of first layer is correct.
        # We do this by comparing against the known analytic result.
        # First layer includes the non_parametrized_layer,
        # followed by observables corresponding to generators of:
        #   qml.RX(x, wires=0)
        #   qml.RY(y, wires=1)
        #   qml.RZ(z, wires=2)

        G1 = np.zeros([3, 3])

        # diag elements
        G1[0, 0] = np.sin(a) ** 2 / 4
        G1[1, 1] = (
            16 * np.cos(a) ** 2 * np.sin(b) ** 3 * np.cos(b) * np.sin(2 * c)
            + np.cos(2 * b) * (2 - 8 * np.cos(a) ** 2 * np.sin(b) ** 2 * np.cos(2 * c))
            + np.cos(2 * (a - b))
            + np.cos(2 * (a + b))
            - 2 * np.cos(2 * a)
            + 14
        ) / 64
        G1[2, 2] = (3 - np.cos(2 * a) - 2 * np.cos(a) ** 2 * np.cos(2 * (b + c))) / 16

        # off diag elements
        G1[0, 1] = np.sin(a) ** 2 * np.sin(b) * np.cos(b + c) / 4
        G1[0, 2] = np.sin(a) ** 2 * np.cos(b + c) / 4
        G1[1, 2] = (
            -np.sin(b)
            * (
                np.cos(2 * (a - b - c))
                + np.cos(2 * (a + b + c))
                + 2 * np.cos(2 * a)
                + 2 * np.cos(2 * (b + c))
                - 6
            )
            / 32
        )

        G1[1, 0] = G1[0, 1]
        G1[2, 0] = G1[0, 2]
        G1[2, 1] = G1[1, 2]

        assert np.allclose(G[:3, :3], G1, atol=tol, rtol=0)

        # =============================================
        # Test block-diag metric tensor of second layer is correct.
        # We do this by computing the required expectation values
        # numerically using multiple circuits.
        # The second layer includes the non_parametrized_layer,
        # RX, RY, RZ gates (x, y, z params), and a 2nd non_parametrized_layer.
        #
        # Observables are the generators of:
        #   qml.RY(f, wires=1)
        #   qml.RZ(g, wires=2)
        G2 = np.zeros([2, 2])

        def layer2_diag(x, y, z, h, g, f):
            non_parametrized_layer(a, b, c)
            qml.RX(x, wires=0)
            qml.RY(y, wires=1)
            qml.RZ(z, wires=2)
            non_parametrized_layer(a, b, c)
            return qml.var(qml.PauliZ(2)), qml.var(qml.PauliY(1))

        layer2_diag = qml.QNode(layer2_diag, dev)

        def layer2_off_diag_first_order(x, y, z, h, g, f):
            non_parametrized_layer(a, b, c)
            qml.RX(x, wires=0)
            qml.RY(y, wires=1)
            qml.RZ(z, wires=2)
            non_parametrized_layer(a, b, c)
            return qml.expval(qml.PauliZ(2)), qml.expval(qml.PauliY(1))

        layer2_off_diag_first_order = qml.QNode(layer2_off_diag_first_order, dev)

        def layer2_off_diag_second_order(x, y, z, h, g, f):
            non_parametrized_layer(a, b, c)
            qml.RX(x, wires=0)
            qml.RY(y, wires=1)
            qml.RZ(z, wires=2)
            non_parametrized_layer(a, b, c)
            return qml.expval(qml.Hermitian(np.kron(Z, Y), wires=[2, 1]))

        layer2_off_diag_second_order = qml.QNode(layer2_off_diag_second_order, dev)

        # calculate the diagonal terms
        varK0, varK1 = layer2_diag(x, y, z, h, g, f)
        G2[0, 0] = varK0 / 4
        G2[1, 1] = varK1 / 4

        # calculate the off-diagonal terms
        exK0, exK1 = layer2_off_diag_first_order(x, y, z, h, g, f)
        exK01 = layer2_off_diag_second_order(x, y, z, h, g, f)

        G2[0, 1] = (exK01 - exK0 * exK1) / 4
        G2[1, 0] = (exK01 - exK0 * exK1) / 4

        assert np.allclose(G[4:6, 4:6], G2, atol=tol, rtol=0)

        # =============================================
        # Test block-diag metric tensor of third layer is correct.
        # We do this by computing the required expectation values
        # numerically.
        # The third layer includes the non_parametrized_layer,
        # RX, RY, RZ gates (x, y, z params), a 2nd non_parametrized_layer,
        # followed by the qml.RY(f, wires=2) operation.
        #
        # Observable is simply generator of:
        #   qml.RY(f, wires=2)
        #
        # Note: since this layer only consists of a single parameter,
        # only need to compute a single diagonal element.

        def layer3_diag(x, y, z, h, g, f):
            non_parametrized_layer(a, b, c)
            qml.RX(x, wires=0)
            qml.RY(y, wires=1)
            qml.RZ(z, wires=2)
            non_parametrized_layer(a, b, c)
            qml.RY(f, wires=2)
            return qml.var(qml.PauliX(1))

        layer3_diag = qml.QNode(layer3_diag, dev)
        G3 = layer3_diag(x, y, z, h, g, f) / 4
        assert np.allclose(G[3:4, 3:4], G3, atol=tol, rtol=0)

        # ============================================
        # Finally, double check that the entire metric
        # tensor is as computed.

        G_expected = block_diag(G1, G3, G2)
        assert np.allclose(G, G_expected, atol=tol, rtol=0)
    def test_evaluate_diag_approx_metric_tensor(self, sample_circuit, tol):
        """Test that a metric tensor under the
        diagonal approximation evaluates correctly and that the old option
        ``diag_approx`` raises a Warning."""
        dev, circuit, non_parametrized_layer, a, b, c = sample_circuit
        params = [-0.282203, 0.145554, 0.331624, -0.163907, 0.57662, 0.081272]
        x, y, z, h, g, f = params

        G = qml.metric_tensor(circuit, approx="diag")(*params)
        with pytest.warns(UserWarning):
            G_alias = qml.metric_tensor(circuit, diag_approx=True)(*params)

        # ============================================
        # Test block-diag metric tensor of first layer is correct.
        # We do this by comparing against the known analytic result.
        # First layer includes the non_parametrized_layer,
        # followed by observables corresponding to generators of:
        #   qml.RX(x, wires=0)
        #   qml.RY(y, wires=1)
        #   qml.RZ(z, wires=2)

        G1 = np.zeros([3, 3])

        # diag elements
        G1[0, 0] = np.sin(a) ** 2 / 4
        G1[1, 1] = (
            16 * np.cos(a) ** 2 * np.sin(b) ** 3 * np.cos(b) * np.sin(2 * c)
            + np.cos(2 * b) * (2 - 8 * np.cos(a) ** 2 * np.sin(b) ** 2 * np.cos(2 * c))
            + np.cos(2 * (a - b))
            + np.cos(2 * (a + b))
            - 2 * np.cos(2 * a)
            + 14
        ) / 64
        G1[2, 2] = (3 - np.cos(2 * a) - 2 * np.cos(a) ** 2 * np.cos(2 * (b + c))) / 16

        assert np.allclose(G[:3, :3], G1, atol=tol, rtol=0)
        assert np.allclose(G_alias[:3, :3], G1, atol=tol, rtol=0)

        # =============================================
        # Test block-diag metric tensor of second layer is correct.
        # We do this by computing the required expectation values
        # numerically using multiple circuits.
        # The second layer includes the non_parametrized_layer,
        # RX, RY, RZ gates (x, y, z params), and a 2nd non_parametrized_layer.
        #
        # Observables are the generators of:
        #   qml.RY(f, wires=1)
        #   qml.RZ(g, wires=2)
        G2 = np.zeros([2, 2])

        def layer2_diag(x, y, z, h, g, f):
            non_parametrized_layer(a, b, c)
            qml.RX(x, wires=0)
            qml.RY(y, wires=1)
            qml.RZ(z, wires=2)
            non_parametrized_layer(a, b, c)
            return qml.var(qml.PauliZ(2)), qml.var(qml.PauliY(1))

        layer2_diag = qml.QNode(layer2_diag, dev)

        # calculate the diagonal terms
        varK0, varK1 = layer2_diag(x, y, z, h, g, f)
        G2[0, 0] = varK0 / 4
        G2[1, 1] = varK1 / 4

        assert np.allclose(G[4:6, 4:6], G2, atol=tol, rtol=0)
        assert np.allclose(G_alias[4:6, 4:6], G2, atol=tol, rtol=0)

        # =============================================
        # Test metric tensor of third layer is correct.
        # We do this by computing the required expectation values
        # numerically.
        # The third layer includes the non_parametrized_layer,
        # RX, RY, RZ gates (x, y, z params), a 2nd non_parametrized_layer,
        # followed by the qml.RY(f, wires=2) operation.
        #
        # Observable is simply generator of:
        #   qml.RY(f, wires=2)
        #
        # Note: since this layer only consists of a single parameter,
        # only need to compute a single diagonal element.

        def layer3_diag(x, y, z, h, g, f):
            non_parametrized_layer(a, b, c)
            qml.RX(x, wires=0)
            qml.RY(y, wires=1)
            qml.RZ(z, wires=2)
            non_parametrized_layer(a, b, c)
            qml.RY(f, wires=2)
            return qml.var(qml.PauliX(1))

        layer3_diag = qml.QNode(layer3_diag, dev)
        G3 = layer3_diag(x, y, z, h, g, f) / 4
        assert np.allclose(G[3:4, 3:4], G3, atol=tol, rtol=0)
        assert np.allclose(G_alias[3:4, 3:4], G3, atol=tol, rtol=0)

        # ============================================
        # Finally, double check that the entire metric
        # tensor is as computed.

        G_expected = block_diag(G1, G3, G2)
        assert np.allclose(G, G_expected, atol=tol, rtol=0)
        assert np.allclose(G_alias, G_expected, atol=tol, rtol=0)
Esempio n. 23
0
 def cost(weights):
     return qml.metric_tensor(circuit)(weights)[2, 2]
    def test_construct_subcircuit_layers(self):
        """Test correct subcircuits constructed
        when a layer structure exists"""
        dev = qml.device("default.qubit", wires=3)
        params = np.ones([8])

        with qml.tape.QuantumTape() as tape:
            # section 1
            qml.RX(params[0], wires=0)
            # section 2
            qml.RY(params[1], wires=0)
            qml.CNOT(wires=[0, 1])
            qml.CNOT(wires=[1, 2])
            # section 3
            qml.RX(params[2], wires=0)
            qml.RY(params[3], wires=1)
            qml.RZ(params[4], wires=2)
            qml.CNOT(wires=[0, 1])
            qml.CNOT(wires=[1, 2])
            # section 4
            qml.RX(params[5], wires=0)
            qml.RY(params[6], wires=1)
            qml.RZ(params[7], wires=2)
            qml.CNOT(wires=[0, 1])
            qml.CNOT(wires=[1, 2])
            return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(1)), qml.expval(qml.PauliX(2))

        tapes, _ = qml.metric_tensor(tape, approx="block-diag")

        # this circuit should split into 4 independent
        # sections or layers when constructing subcircuits
        assert len(tapes) == 4

        # first layer subcircuit
        assert len(tapes[0].operations) == 1
        assert isinstance(tapes[0].operations[0], qml.Hadamard)  # PauliX decomp

        # second layer subcircuit
        assert len(tapes[1].operations) == 4
        assert isinstance(tapes[1].operations[0], qml.RX)
        # PauliY decomp
        assert isinstance(tapes[1].operations[1], qml.PauliZ)
        assert isinstance(tapes[1].operations[2], qml.S)
        assert isinstance(tapes[1].operations[3], qml.Hadamard)

        # # third layer subcircuit
        assert len(tapes[2].operations) == 8
        assert isinstance(tapes[2].operations[0], qml.RX)
        assert isinstance(tapes[2].operations[1], qml.RY)
        assert isinstance(tapes[2].operations[2], qml.CNOT)
        assert isinstance(tapes[2].operations[3], qml.CNOT)
        # PauliX decomp
        assert isinstance(tapes[2].operations[4], qml.Hadamard)
        # PauliY decomp
        assert isinstance(tapes[2].operations[5], qml.PauliZ)
        assert isinstance(tapes[2].operations[6], qml.S)
        assert isinstance(tapes[2].operations[7], qml.Hadamard)

        # # fourth layer subcircuit
        assert len(tapes[3].operations) == 13
        assert isinstance(tapes[3].operations[0], qml.RX)
        assert isinstance(tapes[3].operations[1], qml.RY)
        assert isinstance(tapes[3].operations[2], qml.CNOT)
        assert isinstance(tapes[3].operations[3], qml.CNOT)
        assert isinstance(tapes[3].operations[4], qml.RX)
        assert isinstance(tapes[3].operations[5], qml.RY)
        assert isinstance(tapes[3].operations[6], qml.RZ)
        assert isinstance(tapes[3].operations[7], qml.CNOT)
        assert isinstance(tapes[3].operations[8], qml.CNOT)
        # PauliX decomp
        assert isinstance(tapes[3].operations[9], qml.Hadamard)
        # PauliY decomp
        assert isinstance(tapes[3].operations[10], qml.PauliZ)
        assert isinstance(tapes[3].operations[11], qml.S)
        assert isinstance(tapes[3].operations[12], qml.Hadamard)
Esempio n. 25
0
    def step_and_cost(
        self, qnode, *args, grad_fn=None, recompute_tensor=True, metric_tensor_fn=None, **kwargs
    ):
        """Update the parameter array :math:`x` with one step of the optimizer and return the
        corresponding objective function value prior to the step.

        Args:
            qnode (QNode): the QNode for optimization
            *args : variable length argument list for qnode
            grad_fn (function): optional gradient function of the
                qnode with respect to the variables ``*args``.
                If ``None``, the gradient function is computed automatically.
                Must return a ``tuple[array]`` with the same number of elements as ``*args``.
                Each array of the tuple should have the same shape as the corresponding argument.
            recompute_tensor (bool): Whether or not the metric tensor should
                be recomputed. If not, the metric tensor from the previous
                optimization step is used.
            metric_tensor_fn (function): Optional metric tensor function
                with respect to the variables ``args``.
                If ``None``, the metric tensor function is computed automatically.
            **kwargs : variable length of keyword arguments for the qnode

        Returns:
            tuple: the new variable values :math:`x^{(t+1)}` and the objective function output
            prior to the step
        """
        # pylint: disable=arguments-differ
        if not isinstance(qnode, (qml.QNode, qml.ExpvalCost)) and metric_tensor_fn is None:
            raise ValueError(
                "The objective function must either be encoded as a single QNode or "
                "an ExpvalCost object for the natural gradient to be automatically computed. "
                "Otherwise, metric_tensor_fn must be explicitly provided to the optimizer."
            )

        if recompute_tensor or self.metric_tensor is None:
            if metric_tensor_fn is None:

                metric_tensor_fn = qml.metric_tensor(qnode, approx=self.approx)

            _metric_tensor = metric_tensor_fn(*args, **kwargs)
            # Reshape metric tensor to be square
            shape = qml.math.shape(_metric_tensor)
            size = qml.math.prod(shape[: len(shape) // 2])
            self.metric_tensor = qml.math.reshape(_metric_tensor, (size, size))
            # Add regularization
            self.metric_tensor = self.metric_tensor + self.lam * qml.math.eye(
                size, like=_metric_tensor
            )

        g, forward = self.compute_grad(qnode, args, kwargs, grad_fn=grad_fn)
        new_args = np.array(self.apply_grad(g, args), requires_grad=True)

        if forward is None:
            forward = qnode(*args, **kwargs)

        # Note: for now, we only have single element lists as the new
        # arguments, but this might change, see TODO below.
        # Once the other approach is implemented, we need to unwrap from list
        # if one argument for a cleaner return.
        # if len(new_args) == 1:
        return new_args[0], forward
g1[0, 1] = (exK0K1 - exK0 * exK1) / 4
g1[1, 0] = g1[0, 1]

##############################################################################
# Putting this altogether, the block-diagonal approximation to the Fubini-Study
# metric tensor for this variational quantum circuit is
from scipy.linalg import block_diag

g = block_diag(g0, g1)
print(np.round(g, 8))

##############################################################################
# PennyLane contains a built-in function for computing the Fubini-Study metric
# tensor, :func:`~.pennylane.metric_tensor`, which
# we can use to verify this result:
print(np.round(qml.metric_tensor(circuit)(params), 8))

##############################################################################
# As opposed to our manual computation, which required 6 different quantum
# evaluations, the PennyLane Fubini-Study metric tensor implementation
# requires only 2 quantum evaluations, one per layer. This is done by
# automatically detecting the layer structure, and noting that every
# observable that must be measured commutes, allowing for simultaneous measurement.
#
# Therefore, by combining the quantum natural gradient optimizer with the analytic
# parameter-shift rule to optimize a variational circuit with :math:`d` parameters
# and :math:`L` parametrized layers, a total of :math:`2d+L` quantum evaluations
# are required per optimization step.
#
# Note that the :func:`~.pennylane.metric_tensor` function also supports computing the diagonal
# approximation to the metric tensor:
def natural_gradient(params):
    """Calculate the natural gradient of the qnode() cost function.

    The code you write for this challenge should be completely contained within this function
    between the # QHACK # comment markers.

    You should evaluate the metric tensor and the gradient of the QNode, and then combine these
    together using the natural gradient definition. The natural gradient should be returned as a
    NumPy array.

    The metric tensor should be evaluated using the equation provided in the problem text. Hint:
    you will need to define a new QNode that returns the quantum state before measurement.

    Args:
        params (np.ndarray): Input parameters, of dimension 6

    Returns:
        np.ndarray: The natural gradient evaluated at the input parameters, of dimension 6
    """

    natural_grad = np.zeros(6)

    # QHACK #
    grad_func = qml.grad(qnode)
    gradient = grad_func(params)[0]
    block_diag_mt = qml.metric_tensor(qnode)(params)
    approx_nat_gradient = np.dot(np.linalg.pinv(block_diag_mt), gradient)

    # print(gradient)
    # print(np.round(block_diag_mt, 8))
    # print(approx_nat_gradient)

    def second_oder_parameter_shift(params, i, j, shift_i, shift_j):
        shifted = params.copy()
        shifted[np.unravel_index(i, shifted.shape)] += shift_i
        shifted[np.unravel_index(j, shifted.shape)] += shift_j
        return shifted

    @qml.qnode(dev)
    def overlap(params, shifted_params):
        variational_circuit(shifted_params)
        qml.inv(qml.template(variational_circuit)(params))
        #obs = qml.expval(qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliZ(2))
        return qml.probs([0, 1, 2])

    # print('\n', overlap(params, params)[0])

    f_matrix = np.zeros([6, 6], dtype=np.float64)
    shift = 0.5 * np.pi

    # print('\n', overlap(params, second_oder_parameter_shift(params, 1, 1, shift, shift))[0])

    for i in range(len(gradient)):
        for j in range(i, len(gradient)):
            pp = overlap(
                params, second_oder_parameter_shift(params, i, j, shift,
                                                    shift))[0]
            mp = overlap(
                params, second_oder_parameter_shift(params, i, j, -shift,
                                                    shift))[0]
            pm = overlap(
                params, second_oder_parameter_shift(params, i, j, shift,
                                                    -shift))[0]
            mm = overlap(
                params,
                second_oder_parameter_shift(params, i, j, -shift, -shift))[0]
            fij = (-pp + mp + pm - mm) / 8.0
            f_matrix[i, j] = fij
            f_matrix[j, i] = fij

    # for i in range(len(gradient)):
    #     shifted = params.copy()
    #     shifted[np.unravel_index(i, shifted.shape)] += shift
    #     fii_prob = 1 - 0.5 * (overlap(params, shifted) + 1.0)
    #     f_matrix[i, i] = fii_prob - 1

    # print(np.round(f_matrix, 8))
    natural_grad = np.dot(np.linalg.pinv(f_matrix), gradient)

    # QHACK #

    return natural_grad
Esempio n. 28
0
def natural_gradient(params):
    """Calculate the natural gradient of the qnode() cost function.

    The code you write for this challenge should be completely contained within this function
    between the # QHACK # comment markers.

    You should evaluate the metric tensor and the gradient of the QNode, and then combine these
    together using the natural gradient definition. The natural gradient should be returned as a
    NumPy array.

    The metric tensor should be evaluated using the equation provided in the problem text. Hint:
    you will need to define a new QNode that returns the quantum state before measurement.

    Args:
        params (np.ndarray): Input parameters, of dimension 6

    Returns:
        np.ndarray: The natural gradient evaluated at the input parameters, of dimension 6
    """

    natural_grad = np.zeros(6)

    # QHACK #
    @qml.qnode(dev)
    def probabs(params, i):
        variational_circuit(params)
        return qml.probs(int(i))

    def state(params):
        qstate = np.zeros([
            8,
        ])

        prob0 = probabs(params, 0)
        prob1 = probabs(params, 1)
        prob2 = probabs(params, 2)

        index = 0
        for i in range(2):
            for j in range(2):
                for k in range(2):
                    qstate[index] = np.abs(
                        np.sqrt((prob0[i] * prob1[j] * prob2[k])))
                    index += 1
        normalise = np.vdot(qstate, qstate)
        normalise = 1 / np.abs((np.sqrt(normalise)))
        qstate = normalise * qstate

        return qstate

    def getTensorValue(params, i, j, qstate):

        params1 = params.copy()
        params1[i], params1[j] = params[i] + np.pi / 2, params[j] + np.pi / 2

        params2 = params.copy()
        params2[i], params2[j] = params[i] + np.pi / 2, params[j] - np.pi / 2

        params3 = params.copy()
        params3[i], params3[j] = params[i] - np.pi / 2, params[j] + np.pi / 2

        params4 = params.copy()
        params4[i], params4[j] = params[i] - np.pi / 2, params[j] - np.pi / 2

        answer = ((-(np.abs(np.vdot(qstate, state(params1))))**2 +
                   (np.abs(np.vdot(qstate, state(params2))))**2 +
                   (np.abs(np.vdot(qstate, state(params3))))**2 -
                   (np.abs(np.vdot(qstate, state(params4))))**2) / 8)

        return answer

    def PST(w, i):
        shifted_g = w.copy()
        shifted_g[i] += np.pi / 2
        pst_g_plus = qnode(shifted_g)

        shifted_g[i] -= np.pi
        pst_g_minus = qnode(shifted_g)

        return (pst_g_plus - pst_g_minus) / (2 * np.sin(np.pi / 2))

    qstate = state(params)
    tensor = [[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0],
              [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]]

    for i in range(6):
        for j in range(6):
            tensor[i][j] = getTensorValue(params, i, j, qstate)

    gradient = np.zeros([
        6,
    ])
    #    for i in range(6):
    #        gradient[i][j] = PST(params, i)

    gradient0 = qml.grad(qnode, argnum=0)

    gradient = gradient0(params)
    #print(gradient)

    #    tensor_inv = np.linalg.inv(tensor)
    a = np.linalg.inv(qml.metric_tensor(qnode)(params))
    natural_grad = np.matmul(a, gradient)
    # QHACK #

    return natural_grad
Esempio n. 29
0
def natural_gradient(params):
    """Calculate the natural gradient of the qnode() cost function.

    The code you write for this challenge should be completely contained within this function
    between the # QHACK # comment markers.

    You should evaluate the metric tensor and the gradient of the QNode, and then combine these
    together using the natural gradient definition. The natural gradient should be returned as a
    NumPy array.

    The metric tensor should be evaluated using the equation provided in the problem text. Hint:
    you will need to define a new QNode that returns the quantum state before measurement.

    Args:
        params (np.ndarray): Input parameters, of dimension 6

    Returns:
        np.ndarray: The natural gradient evaluated at the input parameters, of dimension 6
    """

    natural_grad = np.zeros(6)

    # QHACK #
    #@qml.qnode(dev)

    N = len(params)
    gradient = np.zeros([N], dtype=np.float64)
    Fubini = np.zeros([N, N])

    for i in range(N):

        for j in range(N):

            params2_1 = params.copy()
            params2_2 = params.copy()
            params2_3 = params.copy()
            params2_4 = params.copy()

            params2_1[i] += np.pi / 2
            params2_2[i] += np.pi / 2
            params2_3[i] -= np.pi / 2
            params2_4[i] -= np.pi / 2

            params2_1[j] += np.pi / 2

            params2_2[j] -= np.pi / 2

            params2_3[j] += np.pi / 2
            params2_4[j] -= np.pi / 2
            Fubini[i,
                   j] = (1 / 8) * (-inner_prod(params, params2_1) + inner_prod(
                       params, params2_2) + inner_prod(params, params2_3) -
                                   inner_prod(params, params2_4))

    test = qml.metric_tensor(qnode)
    res_test = test(params)

    #Gradient
    for i in range(N):
        shifted = params.copy()
        shifted[i] = params[i] + np.pi / 2
        forward = qnode(shifted)

        shifted[i] = params[i] - np.pi / 2
        backward = qnode(shifted)

        gradient[i] = (forward - backward) / (2 * np.sin(np.pi / 2))

    Fubini_inv = np.linalg.inv(Fubini)
    test3 = np.linalg.pinv(res_test)
    test3 = test3.dot(gradient)
    res_test_inv = np.linalg.inv(res_test)
    natural_grad = Fubini_inv.dot(gradient)

    # QHACK #

    return natural_grad
 def cost(weights):
     return qml.metric_tensor(circuit, approx="block-diag")(weights)[2, 2]