Skip to content

Commit 0f34055

Browse files
add more tests for pp
1 parent e9f87d7 commit 0f34055

4 files changed

Lines changed: 165 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020

2121
- Add `zeros_like`, `ones_like`, `repeat`, `popc`, `top_k`, `lexsort` method for backends.
2222

23+
- Add new way for expectation: `PauliStringSum2MVP`.
24+
25+
2326
### Fixed
2427

2528
- Fix the breaking logic change in jax from dlpack API, dlcapsule -> tensor.
@@ -32,6 +35,7 @@
3235

3336
- Generalize `scatter` method for backends.
3437

38+
3539
## v1.4.0
3640

3741
### Added

tensorcircuit/pauliprop.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,18 @@ def _cache_z_indices(self) -> None:
156156
self.z_indices_np = np.array(z_indices, dtype=np.int32)
157157
self.z_indices = backend.convert_to_tensor(self.z_indices_np, dtype="int32")
158158

159+
def string_to_code(self, s: Tuple[Tuple[int, ...], Tuple[int, ...]]) -> int:
160+
"""
161+
Convert a Pauli string representation ((qidx, ...), (opcode, ...))
162+
to its index in the basis.
163+
164+
:param s: Pauli string as ((qidx, ...), (opcode, ...)).
165+
:type s: Tuple[Tuple[int, ...], Tuple[int, ...]]
166+
:return: Index in the basis.
167+
:rtype: int
168+
"""
169+
return self.string_to_idx.get(s, self.dim)
170+
159171
def get_ptm_1q(self, u: Any) -> Any:
160172
u = backend.convert_to_tensor(u)
161173
u_dag = backend.conj(backend.transpose(u))
@@ -605,6 +617,30 @@ def get_initial_state(self, structures: Any, weights: Any) -> Any:
605617

606618
return (codes, weights)
607619

620+
def string_to_code(self, s: Tuple[Tuple[int, ...], Tuple[int, ...]]) -> Any:
621+
"""
622+
Convert a Pauli string representation ((qidx, ...), (opcode, ...))
623+
to its bit-packed int64 representation.
624+
625+
:param s: Pauli string as ((qidx, ...), (opcode, ...)).
626+
:type s: Tuple[Tuple[int, ...], Tuple[int, ...]]
627+
:return: Bit-packed int64 tensor.
628+
:rtype: Any
629+
"""
630+
K = backend
631+
qubits, opcodes = s
632+
codes = []
633+
for w in range(self.W):
634+
word = 0
635+
for i in range(32):
636+
q = w * 32 + i
637+
if q in qubits:
638+
idx = qubits.index(q)
639+
op = opcodes[idx]
640+
word = word | (int(op) << (2 * i))
641+
codes.append(word)
642+
return K.convert_to_tensor(np.array(codes, dtype=np.int64), dtype="int64")
643+
608644
def expectation(self, state: Any) -> Any:
609645
"""
610646
Compute the expectation value $\langle 0| O(t) |0 \rangle$.

tensorcircuit/quantum.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1907,9 +1907,6 @@ def heisenberg_hamiltonian(
19071907
return PauliStringSum2Dense(ls, weight, numpy=numpy)
19081908

19091909

1910-
# @TODO(@refraction-ray): Matrix-Free MVM (Matrix-Vector Multiplication)
1911-
1912-
19131910
def PauliStringSum2MVP(
19141911
structures: Sequence[Sequence[int]],
19151912
weights: Sequence[float],

tests/test_pauliprop.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@ def test_initialization(request, backend):
1515
assert pp.neighbor_map.shape == (175 + 1, 4, 4)
1616

1717

18+
@pytest.mark.parametrize("backend", ["npb", "jaxb"])
19+
def test_string_to_code(request, backend):
20+
request.getfixturevalue(backend)
21+
N, k = 4, 2
22+
pp = PauliPropagationEngine(N, k)
23+
from tensorcircuit.pauliprop import SparsePauliPropagationEngine
24+
25+
spp = SparsePauliPropagationEngine(N, k)
26+
27+
s = ((0, 1), (3, 3)) # Z0 Z1
28+
idx = pp.string_to_code(s)
29+
assert pp.basis[idx] == s
30+
31+
code = spp.string_to_code(s)
32+
# Z is 3. Bit pattern for Z at q=0 and q=1:
33+
# word 0: (3 << 0) | (3 << 2) = 3 | 12 = 15
34+
assert tc.backend.numpy(code)[0] == 15
35+
36+
1837
@pytest.mark.parametrize("backend", ["npb", "jaxb"])
1938
def test_initial_state(request, backend):
2039
request.getfixturevalue(backend)
@@ -281,3 +300,109 @@ def layer(c, params):
281300
)
282301

283302
assert np.allclose(K.numpy(val_scan_sparse), K.numpy(val_scan_dense), atol=1e-5)
303+
304+
305+
@pytest.mark.parametrize("backend", ["npb", "jaxb"])
306+
def test_truncation_analytical(request, backend, highp):
307+
"""
308+
Directly verify that for insufficient k, the approximation is exactly the same
309+
as the analytical derived Pauli string and then truncated.
310+
"""
311+
request.getfixturevalue(backend)
312+
K = tc.backend
313+
N, k = 3, 1
314+
theta = 0.6
315+
316+
# Initial state: Z1
317+
# Apply RXX(0, 1)
318+
# Heisenberg: exp(i theta/2 X0 X1) Z1 exp(-i theta/2 X0 X1)
319+
# = cos(theta) Z1 + sin(theta) Y1 X0
320+
# Truncated to k=1: cos(theta) Z1
321+
322+
# 1. Dense Engine
323+
pp = PauliPropagationEngine(N, k)
324+
state = pp.get_initial_state(np.array([[0, 3, 0]]), np.array([1.0]))
325+
state = pp.apply_gate(state, "rxx", [0, 1], {"theta": theta})
326+
327+
idx_z1 = pp.string_to_code(((1,), (3,)))
328+
val = K.numpy(state)[idx_z1]
329+
assert np.allclose(val, np.cos(theta), atol=1e-5)
330+
331+
# All other k=1 terms should be 0
332+
# The total weight should be |cos(theta)|^2 ? No, the state vector stores coefficients.
333+
# Sum of absolute values of k=1 terms should be |cos(theta)|
334+
# Exclude SINK index
335+
assert np.allclose(
336+
np.sum(np.abs(K.numpy(state[:-1]))), np.abs(np.cos(theta)), atol=1e-5
337+
)
338+
339+
# 2. Sparse Engine
340+
from tensorcircuit.pauliprop import SparsePauliPropagationEngine
341+
342+
spp = SparsePauliPropagationEngine(N, k, buffer_size=100)
343+
s_state = spp.get_initial_state(np.array([[0, 3, 0]]), np.array([1.0]))
344+
s_state = spp.apply_gate(s_state, "rxx", [0, 1], {"theta": theta})
345+
346+
# Expectation on |0> state sums only Z strings
347+
# Z1 is the only Z string here
348+
assert np.allclose(K.numpy(spp.expectation(s_state)), np.cos(theta), atol=1e-5)
349+
350+
# Verify that only the Z1 term remains with correct coefficient
351+
# (Sparse engine might have multiple entries or empty ones, but aggregate_and_truncate cleans it)
352+
s_codes, s_coeffs = s_state
353+
assert K.numpy(s_codes[0]) == spp.string_to_code(((1,), (3,)))
354+
assert np.allclose(
355+
K.numpy(K.sum(K.abs(s_coeffs))), np.abs(np.cos(theta)), atol=1e-5
356+
)
357+
358+
359+
@pytest.mark.parametrize("backend", ["npb", "jaxb"])
360+
def test_truncation_and_buffer_sparse(request, backend, highp):
361+
"""
362+
Verify Sparse engine truncation for both k (locality) and buffer_size (number of strings).
363+
"""
364+
request.getfixturevalue(backend)
365+
K = tc.backend
366+
N, k = 4, 2
367+
buffer_size = 2
368+
369+
from tensorcircuit.pauliprop import SparsePauliPropagationEngine
370+
371+
spp = SparsePauliPropagationEngine(N, k, buffer_size=buffer_size)
372+
373+
# Initial: Z0
374+
# Apply RX(0, theta1) -> cos(theta1) Z0 + sin(theta1) Y0
375+
# Apply RXX(0, 1, theta2) ->
376+
# cos(theta1) [cos(theta2) Z0 + sin(theta2) Y0 X1] (loc 1, loc 2)
377+
# + sin(theta1) [cos(theta2) Y0 - sin(theta2) Z0 X1] (loc 1, loc 2)
378+
# All are loc <= 2, so k=2 doesn't truncate yet.
379+
# Total terms: 4.
380+
# But buffer_size = 2. It should keep the 2 terms with largest coefficients.
381+
382+
theta1 = 0.4
383+
theta2 = 0.8
384+
s_state = spp.get_initial_state(np.array([[3, 0, 0, 0]]), np.array([1.0]))
385+
s_state = spp.apply_gate(s_state, "rx", [0], {"theta": theta1})
386+
s_state = spp.apply_gate(s_state, "rxx", [0, 1], {"theta": theta2})
387+
388+
# Expected coefficients before buffer truncation:
389+
# c1 = cos(theta1)cos(theta2) (Z0)
390+
# c2 = cos(theta1)sin(theta2) (Y0 X1)
391+
# c3 = sin(theta1)cos(theta2) (Y0)
392+
# c4 = -sin(theta1)sin(theta2) (Z0 X1)
393+
394+
c = np.array(
395+
[
396+
np.cos(theta1) * np.cos(theta2),
397+
np.cos(theta1) * np.sin(theta2),
398+
np.sin(theta1) * np.cos(theta2),
399+
-np.sin(theta1) * np.sin(theta2),
400+
]
401+
)
402+
c_abs = np.abs(c)
403+
top2_idx = np.argsort(c_abs)[-2:]
404+
expected_sum_abs = np.sum(c_abs[top2_idx])
405+
406+
s_codes, s_coeffs = s_state
407+
print(s_codes)
408+
assert np.allclose(K.numpy(K.sum(K.abs(s_coeffs))), expected_sum_abs, atol=1e-5)

0 commit comments

Comments
 (0)