Example #1
0
class TestDefaultQubitDevice(BaseTest):
    """Test the default qubit device. The test ensures that the device is properly
    applying qubit operations and calculating the correct observables."""
    def setUp(self):
        self.dev = DefaultQubit(wires=2, shots=0)

    def test_operation_map(self):
        """Test that default qubit device supports all PennyLane discrete gates."""
        self.logTestName()

        self.assertEqual(set(qml.ops._qubit__ops__),
                         set(self.dev._operation_map))

    def test_observable_map(self):
        """Test that default qubit device supports all PennyLane discrete observables."""
        self.logTestName()

        self.assertEqual(
            set(qml.ops._qubit__obs__) | {"Identity"},
            set(self.dev._observable_map))

    def test_get_operator_matrix(self):
        """Test the the correct matrix is returned given an operation name"""
        self.logTestName()

        for name, fn in {
                **self.dev._operation_map,
                **self.dev._observable_map,
        }.items():
            try:
                op = getattr(qml.ops, name)
            except AttributeError:
                op = getattr(qml.expval, name)

            p = [0.432423, -0.12312, 0.324][:op.num_params]

            if name == "QubitStateVector":
                p = [np.array([1, 0, 0, 0])]
            elif name == "QubitUnitary":
                p = [U]
            elif name == "Hermitian":
                p = [H]

            res = self.dev._get_operator_matrix(name, p)

            if callable(fn):
                # if the default.qubit is an operation accepting parameters,
                # initialise it using the parameters generated above.
                expected = fn(*p)
            else:
                # otherwise, the operation is simply an array.
                expected = fn

            self.assertAllAlmostEqual(res, expected, delta=self.tol)

    def test_apply(self):
        """Test the application of gates to a state"""
        self.logTestName()
        self.dev.reset()

        # loop through all supported operations
        for gate_name, fn in self.dev._operation_map.items():
            log.debug("\tTesting %s gate...", gate_name)

            # start in the state |00>
            self.dev._state = np.array([1, 0, 0, 0])

            # get the equivalent pennylane operation class
            op = getattr(qml.ops, gate_name)
            # the list of wires to apply the operation to
            w = list(range(op.num_wires))

            if op.par_domain == "A":
                # the parameter is an array
                if gate_name == "QubitStateVector":
                    p = [np.array([1, 0, 1, 1]) / np.sqrt(3)]
                    expected_out = p
                elif gate_name == "QubitUnitary":
                    p = [U]
                    w = [0]
                    expected_out = np.kron(U @ np.array([1, 0]),
                                           np.array([1, 0]))
                elif gate_name == "BasisState":
                    p = [np.array([1, 1])]
                    expected_out = np.array([0, 0, 0, 1])

            elif op.par_domain == "N":
                # the parameter is an integer
                p = [1, 3, 4][:op.num_params]
            else:
                # the parameter is a float
                p = [0.432423, -0.12312, 0.324][:op.num_params]

                if callable(fn):
                    # if the default.qubit is an operation accepting parameters,
                    # initialise it using the parameters generated above.
                    O = fn(*p)
                else:
                    # otherwise, the operation is simply an array.
                    O = fn

                # calculate the expected output
                if op.num_wires == 1:
                    expected_out = np.kron(O @ np.array([1, 0]),
                                           np.array([1, 0]))
                elif op.num_wires == 2:
                    expected_out = O @ self.dev._state

            self.dev.apply(gate_name, wires=w, par=p)

            # verify the device is now in the expected state
            self.assertAllAlmostEqual(self.dev._state,
                                      expected_out,
                                      delta=self.tol)

    def test_apply_errors(self):
        """Test that apply fails for incorrect state preparation, and > 2 qubit gates"""
        self.logTestName()

        with self.assertRaisesRegex(
                ValueError, r"State vector must be of length 2\*\*wires."):
            p = [np.array([1, 0, 1, 1, 1]) / np.sqrt(3)]
            self.dev.apply("QubitStateVector", wires=[0, 1], par=[p])

        with self.assertRaisesRegex(
                ValueError,
                "BasisState parameter must be an array of 0 or 1 integers of length at most 2.",
        ):
            self.dev.apply("BasisState",
                           wires=[0, 1],
                           par=[np.array([-0.2, 4.2])])

        with self.assertRaisesRegex(
                ValueError,
                "The default.qubit plugin can apply BasisState only to all of the 2 wires.",
        ):
            self.dev.apply("BasisState",
                           wires=[0, 1, 2],
                           par=[np.array([0, 1])])

    def test_ev(self):
        """Test that expectation values are calculated correctly"""
        self.logTestName()
        self.dev.reset()

        # loop through all supported observables
        for name, fn in self.dev._observable_map.items():
            log.debug("\tTesting %s observable...", name)

            # start in the state |00>
            self.dev._state = np.array([1, 0, 1, 1]) / np.sqrt(3)

            # get the equivalent pennylane operation class
            op = getattr(qml.ops, name)

            if op.par_domain == "A":
                # the parameter is an array
                p = [H]
            else:
                # the parameter is a float
                p = [0.432423, -0.12312, 0.324][:op.num_params]

            if callable(fn):
                # if the default.qubit is an operation accepting parameters,
                # initialise it using the parameters generated above.
                O = fn(*p)
            else:
                # otherwise, the operation is simply an array.
                O = fn

            # calculate the expected output
            if op.num_wires == 1 or op.num_wires == 0:
                expected_out = self.dev._state.conj() @ np.kron(
                    O, I) @ self.dev._state
            elif op.num_wires == 2:
                expected_out = self.dev._state.conj() @ O @ self.dev._state
            else:
                raise NotImplementedError(
                    "Test for operations with num_wires=" + op.num_wires +
                    " not implemented.")

            res = self.dev.ev(O, wires=[0])

            # verify the device is now in the expected state
            self.assertAllAlmostEqual(res, expected_out, delta=self.tol)

            # text warning raised if matrix is complex
            with pytest.warns(RuntimeWarning,
                              match='Nonvanishing imaginary part'):
                self.dev.ev(H + 1j, [0])

    def test_var_pauliz(self):
        """Test that variance of PauliZ is the same as I-<Z>^2"""
        self.logTestName()
        self.dev.reset()

        phi = 0.543
        theta = 0.6543
        self.dev.apply('RX', wires=[0], par=[phi])
        self.dev.apply('RY', wires=[0], par=[theta])

        var = self.dev.var('PauliZ', [0], [])
        mean = self.dev.expval('PauliZ', [0], [])

        self.assertAlmostEqual(var, 1 - mean**2, delta=self.tol)

    def test_var_pauliz_rotated_state(self):
        """test correct variance for <Z> of a rotated state"""
        self.logTestName()
        self.dev.reset()

        phi = 0.543
        theta = 0.6543
        self.dev.apply('RX', wires=[0], par=[phi])
        self.dev.apply('RY', wires=[0], par=[theta])
        var = self.dev.var('PauliZ', [0], [])
        expected = 0.25 * (3 - np.cos(2 * theta) -
                           2 * np.cos(theta)**2 * np.cos(2 * phi))
        self.assertAlmostEqual(var, expected, delta=self.tol)

    def test_var_hermitian_rotated_state(self):
        """test correct variance for <H> of a rotated state"""
        self.logTestName()
        self.dev.reset()

        phi = 0.543
        theta = 0.6543

        H = np.array([[4, -1 + 6j], [-1 - 6j, 2]])
        self.dev.apply('RX', wires=[0], par=[phi])
        self.dev.apply('RY', wires=[0], par=[theta])
        var = self.dev.var('Hermitian', [0], [H])
        expected = 0.5*(2*np.sin(2*theta)*np.cos(phi)**2+24*np.sin(phi)\
                    *np.cos(phi)*(np.sin(theta)-np.cos(theta))+35*np.cos(2*phi)+39)
        self.assertAlmostEqual(var, expected, delta=self.tol)
class TestDefaultQubitDevice(BaseTest):
    """Test the default qubit device. The test ensures that the device is properly
    applying qubit operations and calculating the correct observables."""
    def setUp(self):
        self.dev = DefaultQubit(wires=2, shots=0)

    def test_operation_map(self):
        """Test that default qubit device supports all PennyLane discrete gates."""
        self.logTestName()

        self.assertEqual(set(qml.ops.qubit.__all__),
                         set(self.dev._operation_map))

    def test_expectation_map(self):
        """Test that default qubit device supports all PennyLane discrete expectations."""
        self.logTestName()

        self.assertEqual(
            set(qml.expval.qubit.__all__) | {"Identity"},
            set(self.dev._expectation_map))

    def test_expand_one(self):
        """Test that a 1 qubit gate correctly expands to 3 qubits."""
        self.logTestName()

        dev = DefaultQubit(wires=3)

        # test applied to wire 0
        res = dev.expand_one(U, [0])
        expected = np.kron(np.kron(U, I), I)
        self.assertAllAlmostEqual(res, expected, delta=self.tol)

        # test applied to wire 1
        res = dev.expand_one(U, [1])
        expected = np.kron(np.kron(I, U), I)
        self.assertAllAlmostEqual(res, expected, delta=self.tol)

        # test applied to wire 2
        res = dev.expand_one(U, [2])
        expected = np.kron(np.kron(I, I), U)
        self.assertAllAlmostEqual(res, expected, delta=self.tol)

        # test exception raised if U is not 2x2 matrix
        with self.assertRaisesRegex(ValueError, "2x2 matrix required"):
            dev.expand_one(U2, [0])

        # test exception raised if more than one subsystem provided
        with self.assertRaisesRegex(ValueError,
                                    "One target subsystem required"):
            dev.expand_one(U, [0, 1])

    def test_expand_two(self):
        """Test that a 2 qubit gate correctly expands to 3 qubits."""
        self.logTestName()

        dev = DefaultQubit(wires=4)

        # test applied to wire 0+1
        res = dev.expand_two(U2, [0, 1])
        expected = np.kron(np.kron(U2, I), I)
        self.assertAllAlmostEqual(res, expected, delta=self.tol)

        # test applied to wire 1+2
        res = dev.expand_two(U2, [1, 2])
        expected = np.kron(np.kron(I, U2), I)
        self.assertAllAlmostEqual(res, expected, delta=self.tol)

        # test applied to wire 2+3
        res = dev.expand_two(U2, [2, 3])
        expected = np.kron(np.kron(I, I), U2)
        self.assertAllAlmostEqual(res, expected, delta=self.tol)

        # CNOT with target on wire 1
        res = dev.expand_two(CNOT, [1, 0])
        rows = np.array([0, 2, 1, 3])
        expected = np.kron(np.kron(CNOT[:, rows][rows], I), I)
        self.assertAllAlmostEqual(res, expected, delta=self.tol)

        # test exception raised if U is not 4x4 matrix
        with self.assertRaisesRegex(ValueError, "4x4 matrix required"):
            dev.expand_two(U, [0])

        # test exception raised if two subsystems are not provided
        with self.assertRaisesRegex(ValueError,
                                    "Two target subsystems required"):
            dev.expand_two(U2, [0])

        # test exception raised if unphysical subsystems provided
        with self.assertRaisesRegex(ValueError, "Bad target subsystems."):
            dev.expand_two(U2, [-1, 5])

    def test_expand_multi(self):
        """Test that any arbitrary qubit gate correctly expands to the full
        system."""
        pass

    def test_get_operator_matrix(self):
        """Test the the correct matrix is returned given an operation name"""
        self.logTestName()

        for name, fn in {
                **self.dev._operation_map,
                **self.dev._expectation_map,
        }.items():
            try:
                op = qml.ops.__getattribute__(name)
            except AttributeError:
                op = qml.expval.__getattribute__(name)

            p = [0.432423, -0.12312, 0.324][:op.num_params]

            if name == "QubitStateVector":
                p = [np.array([1, 0, 0, 0])]
            elif name == "QubitUnitary":
                p = [U]
            elif name == "Hermitian":
                p = [H]

            res = self.dev._get_operator_matrix(name, p)

            if callable(fn):
                # if the default.qubit is an operation accepting parameters,
                # initialise it using the parameters generated above.
                expected = fn(*p)
            else:
                # otherwise, the operation is simply an array.
                expected = fn

            self.assertAllAlmostEqual(res, expected, delta=self.tol)

    def test_apply(self):
        """Test the application of gates to a state"""
        self.logTestName()
        self.dev.reset()

        # loop through all supported operations
        for gate_name, fn in self.dev._operation_map.items():
            log.debug("\tTesting %s gate...", gate_name)

            # start in the state |00>
            self.dev._state = np.array([1, 0, 0, 0])

            # get the equivalent pennylane operation class
            op = qml.ops.__getattribute__(gate_name)
            # the list of wires to apply the operation to
            w = list(range(op.num_wires))

            if op.par_domain == "A":
                # the parameter is an array
                if gate_name == "QubitStateVector":
                    p = [np.array([1, 0, 1, 1]) / np.sqrt(3)]
                    expected_out = p
                elif gate_name == "QubitUnitary":
                    p = [U]
                    w = [0]
                    expected_out = np.kron(U @ np.array([1, 0]),
                                           np.array([1, 0]))
                elif gate_name == "BasisState":
                    p = [np.array([1, 1])]
                    expected_out = np.array([0, 0, 0, 1])

            elif op.par_domain == "N":
                # the parameter is an integer
                p = [1, 3, 4][:op.num_params]
            else:
                # the parameter is a float
                p = [0.432423, -0.12312, 0.324][:op.num_params]

                if callable(fn):
                    # if the default.qubit is an operation accepting parameters,
                    # initialise it using the parameters generated above.
                    O = fn(*p)
                else:
                    # otherwise, the operation is simply an array.
                    O = fn

                # calculate the expected output
                if op.num_wires == 1:
                    expected_out = np.kron(O @ np.array([1, 0]),
                                           np.array([1, 0]))
                elif op.num_wires == 2:
                    expected_out = O @ self.dev._state

            self.dev.apply(gate_name, wires=w, par=p)

            # verify the device is now in the expected state
            self.assertAllAlmostEqual(self.dev._state,
                                      expected_out,
                                      delta=self.tol)

    def test_apply_errors(self):
        """Test that apply fails for incorrect state preparation, and > 2 qubit gates"""
        self.logTestName()

        with self.assertRaisesRegex(
                ValueError, r"State vector must be of length 2\*\*wires."):
            p = [np.array([1, 0, 1, 1, 1]) / np.sqrt(3)]
            self.dev.apply("QubitStateVector", wires=[0, 1], par=[p])

        with self.assertRaisesRegex(
                ValueError,
                "BasisState parameter must be an array of 0 or 1 integers of length at most 2.",
        ):
            self.dev.apply("BasisState",
                           wires=[0, 1],
                           par=[np.array([-0.2, 4.2])])

        with self.assertRaisesRegex(
                ValueError,
                "The default.qubit plugin can apply BasisState only to all of the 2 wires.",
        ):
            self.dev.apply("BasisState",
                           wires=[0, 1, 2],
                           par=[np.array([0, 1])])

        with self.assertRaisesRegex(
                ValueError,
                "This plugin supports only one- and two-qubit gates."):
            self.dev.apply("QubitUnitary", wires=[0, 1, 2], par=[U2])

    def test_ev(self):
        """Test that expectation values are calculated correctly"""
        self.logTestName()
        self.dev.reset()

        # loop through all supported observables
        for name, fn in self.dev._expectation_map.items():
            print(name)
            log.debug("\tTesting %s observable...", name)

            # start in the state |00>
            self.dev._state = np.array([1, 0, 1, 1]) / np.sqrt(3)

            # get the equivalent pennylane operation class
            op = qml.expval.__getattribute__(name)

            if op.par_domain == "A":
                # the parameter is an array
                p = [H]
            else:
                # the parameter is a float
                p = [0.432423, -0.12312, 0.324][:op.num_params]

            if callable(fn):
                # if the default.qubit is an operation accepting parameters,
                # initialise it using the parameters generated above.
                O = fn(*p)
            else:
                # otherwise, the operation is simply an array.
                O = fn

            print("op.num_wires=" + str(op.num_wires))

            # calculate the expected output
            if op.num_wires == 1 or op.num_wires == 0:
                expected_out = self.dev._state.conj() @ np.kron(
                    O, I) @ self.dev._state
            elif op.num_wires == 2:
                expected_out = self.dev._state.conj() @ O @ self.dev._state
            else:
                raise NotImplementedError(
                    "Test for operations with num_wires=" + op.num_wires +
                    " not implemented.")

            res = self.dev.ev(O, wires=[0])

            # verify the device is now in the expected state
            self.assertAllAlmostEqual(res, expected_out, delta=self.tol)

            # text exception raised if matrix is not 2x2 or 4x4
            with self.assertRaisesRegex(
                    ValueError,
                    "Only one and two-qubit expectation is supported."):
                self.dev.ev(U_toffoli, [0])

            # text warning raised if matrix is complex
            with self.assertLogs(level="WARNING") as l:
                self.dev.ev(H + 1j, [0])
                self.assertEqual(len(l.output), 1)
                self.assertEqual(len(l.records), 1)
                self.assertIn("Nonvanishing imaginary part", l.output[0])