File size: 6,822 Bytes
2a87ede
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
"""

Unit tests for dense_evolution/observables.py -- pauli_expectation and

pauli_sum_expectation, cross-checked against brute-force dense Pauli

matrices (kron products), not just against their own derivation.

"""
import numpy as np
import pytest

from dense_evolution import DenseSVSimulator
from dense_evolution.observables import pauli_expectation, pauli_sum_expectation, pauli_hamiltonian_to_matrix

_PAULI_MATS = {
    'I': np.eye(2, dtype=complex),
    'X': np.array([[0, 1], [1, 0]], dtype=complex),
    'Y': np.array([[0, -1j], [1j, 0]], dtype=complex),
    'Z': np.array([[1, 0], [0, -1]], dtype=complex),
}


def _brute_force(psi, pauli_string):
    """Independent reference: build the full 2**n Pauli matrix via kron

    and compute <psi|P|psi> directly -- pauli_string[0] is the most

    significant factor, matching this package's qubit-0-is-MSB layout."""
    op = _PAULI_MATS[pauli_string[0]]
    for c in pauli_string[1:]:
        op = np.kron(op, _PAULI_MATS[c])
    return float(np.real(np.conj(psi) @ op @ psi))


class TestPauliExpectation:

    def test_matches_brute_force_dense_matrices(self):
        rng = np.random.default_rng(42)
        n_qubits, dim = 4, 16
        letters = ['I', 'X', 'Y', 'Z']
        for _ in range(200):
            psi = rng.normal(size=dim) + 1j * rng.normal(size=dim)
            psi /= np.linalg.norm(psi)
            pauli_string = ''.join(rng.choice(letters) for _ in range(n_qubits))
            assert pauli_expectation(psi, pauli_string) == pytest.approx(
                _brute_force(psi, pauli_string), abs=1e-9)

    def test_identity_string_returns_norm(self):
        rng = np.random.default_rng(1)
        psi = rng.normal(size=8) + 1j * rng.normal(size=8)
        psi /= np.linalg.norm(psi)
        assert pauli_expectation(psi, 'III') == pytest.approx(1.0, abs=1e-9)

    def test_dict_form_matches_string_form(self):
        rng = np.random.default_rng(2)
        psi = rng.normal(size=16) + 1j * rng.normal(size=16)
        psi /= np.linalg.norm(psi)
        assert pauli_expectation(psi, {2: 'Z'}) == pytest.approx(
            pauli_expectation(psi, 'IIZI'), abs=1e-9)

    def test_pair_iterable_form_matches_string_form(self):
        rng = np.random.default_rng(3)
        psi = rng.normal(size=16) + 1j * rng.normal(size=16)
        psi /= np.linalg.norm(psi)
        assert pauli_expectation(psi, [(0, 'X'), (3, 'Z')]) == pytest.approx(
            pauli_expectation(psi, 'XIIZ'), abs=1e-9)

    def test_bell_state_zz_is_perfectly_correlated(self):
        sim = DenseSVSimulator(2, use_gpu=False, use_float32=False)
        sim.run_circuit([('h', 0), ('cx', 0, 1)])
        assert pauli_expectation(sim.get_statevector(), 'ZZ') == pytest.approx(1.0, abs=1e-9)

    def test_bell_state_xx_is_perfectly_correlated(self):
        sim = DenseSVSimulator(2, use_gpu=False, use_float32=False)
        sim.run_circuit([('h', 0), ('cx', 0, 1)])
        assert pauli_expectation(sim.get_statevector(), 'XX') == pytest.approx(1.0, abs=1e-9)

    def test_ghz3_zzz_vanishes(self):
        # <GHZ3|ZZZ|GHZ3> = 0.5*(<000|ZZZ|000> + <111|ZZZ|111>) = 0.5*(1-1) = 0
        sim = DenseSVSimulator(3, use_gpu=False, use_float32=False)
        sim.run_circuit([('h', 0), ('cx', 0, 1), ('cx', 1, 2)])
        assert pauli_expectation(sim.get_statevector(), 'ZZZ') == pytest.approx(0.0, abs=1e-9)

    def test_string_length_mismatch_raises(self):
        with pytest.raises(ValueError):
            pauli_expectation(np.array([1.0, 0, 0, 0]), 'ZZZ', n_qubits=2)

    def test_unknown_pauli_letter_raises(self):
        with pytest.raises(ValueError):
            pauli_expectation(np.array([1.0, 0, 0, 0]), {0: 'Q'})

    def test_qubit_out_of_range_raises(self):
        with pytest.raises(ValueError):
            pauli_expectation(np.array([1.0, 0, 0, 0]), {5: 'Z'})


class TestPauliSumExpectation:

    def test_matches_manual_weighted_sum(self):
        rng = np.random.default_rng(7)
        psi = rng.normal(size=16) + 1j * rng.normal(size=16)
        psi /= np.linalg.norm(psi)
        terms = [(1.0, 'ZZII'), (0.5, {0: 'X'}), (-0.3, 'IYYI')]
        expected = sum(c * pauli_expectation(psi, p) for c, p in terms)
        assert pauli_sum_expectation(psi, terms) == pytest.approx(expected, abs=1e-9)

    def test_empty_terms_is_zero(self):
        psi = np.array([1.0, 0, 0, 0])
        assert pauli_sum_expectation(psi, []) == 0.0


class TestPauliHamiltonianToMatrix:

    def test_matches_brute_force_dense_matrices(self):
        rng = np.random.default_rng(11)
        n_qubits, dim = 4, 16
        letters = ['I', 'X', 'Y', 'Z']
        for _ in range(50):
            n_terms = rng.integers(1, 6)
            terms = []
            expected = np.zeros((dim, dim), dtype=complex)
            for _ in range(n_terms):
                coeff = float(rng.normal())
                pauli_string = ''.join(rng.choice(letters) for _ in range(n_qubits))
                terms.append((coeff, pauli_string))
                op = _PAULI_MATS[pauli_string[0]]
                for c in pauli_string[1:]:
                    op = np.kron(op, _PAULI_MATS[c])
                expected += coeff * op
            H = pauli_hamiltonian_to_matrix(terms, n_qubits=n_qubits)
            assert np.allclose(H, expected, atol=1e-9)

    def test_is_hermitian(self):
        terms = [(1.0, 'ZZ'), (0.5, {0: 'X'}), (-0.3, {1: 'Y'})]
        H = pauli_hamiltonian_to_matrix(terms, n_qubits=2)
        assert np.allclose(H, H.conj().T)

    def test_matches_pauli_sum_expectation_on_a_real_state(self):
        rng = np.random.default_rng(3)
        psi = rng.normal(size=8) + 1j * rng.normal(size=8)
        psi /= np.linalg.norm(psi)
        terms = [(1.0, 'ZZI'), (0.5, {0: 'X'}), (-0.7, {1: 'Y', 2: 'Z'})]
        H = pauli_hamiltonian_to_matrix(terms, n_qubits=3)
        via_matrix = float(np.real(np.conj(psi) @ H @ psi))
        via_direct = pauli_sum_expectation(psi, terms)
        assert via_matrix == pytest.approx(via_direct, abs=1e-9)

    def test_bell_state_zz_ground_truth(self):
        sim = DenseSVSimulator(2, use_gpu=False, use_float32=False)
        sim.run_circuit([('h', 0), ('cx', 0, 1)])
        psi = sim.get_statevector()
        H = pauli_hamiltonian_to_matrix([(1.0, 'ZZ')], n_qubits=2)
        assert float(np.real(np.conj(psi) @ H @ psi)) == pytest.approx(1.0, abs=1e-9)

    def test_qubit_out_of_range_raises(self):
        with pytest.raises(ValueError):
            pauli_hamiltonian_to_matrix([(1.0, {5: 'Z'})], n_qubits=2)

    def test_invalid_n_qubits_raises(self):
        with pytest.raises(ValueError):
            pauli_hamiltonian_to_matrix([(1.0, {0: 'Z'})], n_qubits=0)