@@ -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" ])
1938def 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