1+ import numpy as np
2+ import pytest
3+
4+ from yaqs .core .data_structures .networks import MPO , MPS
5+ from yaqs .core .data_structures .simulation_parameters import Observable , PhysicsSimParams
6+
7+ from yaqs .core .methods .TDVP import (
8+ _split_mps_tensor ,
9+ _merge_mps_tensor_pair ,
10+ _merge_mpo_tensor_pair ,
11+ _contraction_operator_step_right ,
12+ _contraction_operator_step_left ,
13+ _compute_right_operator_blocks ,
14+ _apply_local_hamiltonian ,
15+ _apply_local_bond_contraction ,
16+ _local_hamiltonian_step ,
17+ _local_bond_step ,
18+ single_site_TDVP ,
19+ two_site_TDVP ,
20+ )
21+
22+ def test_split_mps_tensor_left_right_sqrt ():
23+ # Create an input tensor A with shape (d0*d1, D0, D2).
24+ # Let d0 = d1 = 2 so that A.shape[0]=4, and choose D0=3, D2=5.
25+ A = np .random .randn (4 , 3 , 5 )
26+ # For each svd_distr option, run the splitting and then reconstruct A.
27+ for distr in ['left' , 'right' , 'sqrt' ]:
28+ A0 , A1 = _split_mps_tensor (A , svd_distr = distr , threshold = 1e-8 )
29+ # A0 should have shape (2, 3, r) and A1 should have shape (2, r, 5),
30+ # where r is the effective rank.
31+ assert A0 .ndim == 3
32+ assert A1 .ndim == 3
33+ r = A0 .shape [2 ]
34+ assert A1 .shape [1 ] == r
35+ # Reconstruct A: undo the transpose on A1 then contract A0 and A1.
36+ A1_recon = A1 .transpose ((1 , 0 , 2 )) # now shape (r, 2, 5)
37+ # Contract along the rank index:
38+ # A0 has indices: (d0, D0, r), A1_recon has indices: (r, d1, D2).
39+ # Form a tensor of shape (d0, d1, D0, D2) then reshape back to (4, 3, 5)
40+ A_recon = np .tensordot (A0 , A1_recon , axes = (2 , 0 )) # shape (2, 3, 2, 5)
41+ A_recon = A_recon .transpose ((0 ,2 ,1 ,3 )).reshape (4 , 3 , 5 )
42+ # Up to SVD sign and ordering ambiguities, the reconstruction should be close.
43+ np .testing .assert_allclose (A , A_recon , atol = 1e-6 )
44+
45+ def test_split_mps_tensor_invalid_shape ():
46+ # If A.shape[0] is not divisible by 2, the function should raise a ValueError.
47+ A = np .random .randn (3 , 3 , 5 )
48+ with pytest .raises (ValueError ):
49+ _split_mps_tensor (A , svd_distr = 'left' )
50+
51+ def test_merge_mps_tensor_pair ():
52+ # Let A0 have shape (2, 3, 4) and A1 have shape (5, 4, 7).
53+ # In _merge_mps_tensor_pair the arrays are interpreted with label tuples
54+ # (0,2,3) for A0 and (1,3,4) for A1, so contraction is over the third axis of A0 and
55+ # the second axis of A1.
56+ A0 = np .random .randn (2 , 3 , 4 )
57+ A1 = np .random .randn (5 , 4 , 7 )
58+ merged = _merge_mps_tensor_pair (A0 , A1 )
59+ # Expected shape: first two axes merged from A0[0] and A1[0]:
60+ # output shape = ((2*5), 3, 7) i.e. (10, 3, 7)
61+ assert merged .shape == (10 , 3 , 7 )
62+
63+ def test_merge_mpo_tensor_pair ():
64+ # Let A0 be a 4D array with shape (2, 3, 4, 5) and
65+ # A1 be a 4D array with shape (7, 8, 5, 9).
66+ # The contract call uses label tuples (0,2,4,6) and (1,3,6,5) and contracts label 6.
67+ A0 = np .random .randn (2 , 3 , 4 , 5 )
68+ A1 = np .random .randn (7 , 8 , 5 , 9 )
69+ merged = _merge_mpo_tensor_pair (A0 , A1 )
70+ # Expected shape: merge first two axes of the result, where result (before reshape)
71+ # should have shape (2,7,3,8,4,9), then merged to (2*7, 3*8, 4,9) = (14,24,4,9).
72+ assert merged .shape == (14 , 24 , 4 , 9 )
73+
74+ def test_contraction_operator_step_right ():
75+ # Choose shapes as described in the function.
76+ A = np .random .randn (2 , 3 , 4 )
77+ # R: contract A's last axis (size 4) with R's first axis.
78+ R = np .random .randn (4 , 5 , 6 )
79+ # W must have shape with axes (1,3) matching T from tensordot(A, R, 1) of shape (2,3,5,6):
80+ # We require W.shape[1]=2 and W.shape[3]=5. Let W = (7,2,8,5)
81+ W = np .random .randn (7 , 2 , 8 , 5 )
82+ # B: choose shape so that B.conj() has axes (0,2) matching T from later step.
83+ # After the previous steps, T becomes shape (3,8,7,6); we contract with B.conj() axes ((2,3),(0,2)).
84+ # So let B be of shape (7, 9, 6).
85+ B = np .random .randn (7 , 9 , 6 )
86+ Rnext = _contraction_operator_step_right (A , B , W , R )
87+ # Expected shape: (3,8,9) (from the discussion above).
88+ assert Rnext .shape == (3 , 8 , 9 )
89+
90+ def test_contraction_operator_step_left ():
91+ # Set up dummy arrays with shapes so that the contraction is valid.
92+ # Let A be shape (3,4,10)
93+ A = np .random .randn (3 , 4 , 10 )
94+ # Let B be shape (7, 6, 8) so that B.conj() has shape (7,6,8).
95+ B = np .random .randn (7 , 6 , 8 )
96+ # Let L be shape (4,5,6) and require that L.shape[2] (6) matches B.shape[1] (6).
97+ L_arr = np .random .randn (4 , 5 , 6 )
98+ # Let W be shape (7,3,5,9) so that we contract axes ((0,2),(2,1)) with T later.
99+ W = np .random .randn (7 , 3 , 5 , 9 )
100+ Rnext = _contraction_operator_step_left (A , B , W , L_arr )
101+ # The expected shape from our reasoning is (10,9,8) (A's remaining axis 2 becomes output along with leftover dims from T).
102+ # We check that the result is 3-dimensional.
103+ assert Rnext .ndim == 3
104+
105+ def test_apply_local_hamiltonian ():
106+ # Let A: shape (2,3,4); R: shape (4,5,6) as before.
107+ A = np .random .randn (2 , 3 , 4 )
108+ R = np .random .randn (4 , 5 , 6 )
109+ # Let W be shape (7,2,8,5) as in test above.
110+ W = np .random .randn (7 , 2 , 8 , 5 )
111+ # Let L be shape (3,8,9) so that tensordot works.
112+ L_arr = np .random .randn (3 , 8 , 9 )
113+ out = _apply_local_hamiltonian (L_arr , R , W , A )
114+ # The function transposes the final result so we expect a 3D array.
115+ assert out .ndim == 3
116+
117+ def test_apply_local_bond_contraction ():
118+ # Let C: shape (2,3)
119+ C = np .random .randn (2 , 3 )
120+ # Let R: shape (3,4,5)
121+ R = np .random .randn (3 , 4 , 5 )
122+ # Let L: shape (2,4,6)
123+ L_arr = np .random .randn (2 , 4 , 6 )
124+ out = _apply_local_bond_contraction (L_arr , R , C )
125+ # Expected output shape: contraction gives shape (6,5)
126+ assert out .shape == (6 , 5 )
127+
128+ def test_local_hamiltonian_step ():
129+ # We choose an MPS tensor A with shape (d0, d0, d1) where d0=2, d1=4.
130+ # (The requirement here is that the first two dimensions are equal,
131+ # so that after the contraction chain the operator is square.)
132+ A = np .random .randn (2 , 2 , 4 ) # total elements: 2*2*4 = 16
133+ # Choose R of shape (d1, X, d1) with d1=4 and X=1.
134+ R = np .random .randn (4 , 1 , 4 ) # shape: (4,1,4)
135+ # Choose W of shape (d0, d0, X, X) with d0=2 and X=1.
136+ W = np .random .randn (2 , 2 , 1 , 1 ) # shape: (2,2,1,1)
137+ # Choose L so that the contraction makes sense.
138+ # In our contraction chain, after:
139+ # T1 = tensordot(A, R, axes=1) → shape (2,2,1,4)
140+ # T2 = tensordot(W, T1, axes=((1,3),(0,2))) → shape (2,1,2,4)
141+ # T3 = T2.transpose((2,1,0,3)) → shape (2,1,2,4) (here the permutation reorders axes)
142+ # Then we contract T3 with L along axes ((2,1),(0,1)).
143+ # To contract T3’s axes (axis2, axis1) = (2,1) we need L with shape (2,1,r).
144+ # Then T4 will have shape (remaining T3 axes: (axis0, axis3)) plus L’s remaining axis, i.e. (2,4,r).
145+ # Finally, a transpose (here, (0,2,1)) gives shape (2, r, 4).
146+ # We want the final shape to equal A’s shape (2,2,4), so we set r=2.
147+ L_arr = np .random .randn (2 , 1 , 2 ) # shape: (2,1,2)
148+ dt = 0.05
149+ numiter = 10
150+ out = _local_hamiltonian_step (L_arr , R , W , A , dt , numiter )
151+ # The operator should be square, so out.shape should equal A.shape.
152+ assert out .shape == A .shape , f"Expected shape { A .shape } , got { out .shape } "
153+
154+ def test_local_bond_step ():
155+ # For the bond step we want the operator to be square.
156+ # Let C be a matrix of shape (p, q). We choose C to be square.
157+ C = np .random .randn (2 , 2 ) # total elements: 4
158+ # Choose R of shape (q, r, s). To contract C and get back the same number of elements,
159+ # we can choose R such that q=2, r=2, s=2.
160+ R = np .random .randn (2 , 2 , 2 ) # shape: (2,2,2)
161+ # Choose L of shape (p, r, t) with p=2 and r=2.
162+ # We want the final result to have shape (p, q) = (2,2); so t must equal q=2.
163+ L_arr = np .random .randn (2 , 2 , 2 ) # shape: (2,2,2)
164+ dt = 0.05
165+ numiter = 10
166+ out = _local_bond_step (L_arr , R , C , dt , numiter )
167+ # The output shape should equal the input shape.
168+ assert out .shape == C .shape , f"Expected shape { C .shape } , got { out .shape } "
169+
170+ def test_single_site_TDVP ():
171+ L = 5
172+ d = 2
173+ J = 1
174+ g = 0.5
175+ H = MPO ()
176+ H .init_Ising (L , d , J , g )
177+ state = MPS (L )
178+ measurements = [Observable ('z' , site ) for site in range (L )]
179+ sim_params = PhysicsSimParams (measurements , T = 0.2 , dt = 0.1 , sample_timesteps = True , N = 1 , max_bond_dim = 4 , threshold = 1e-6 , order = 1 )
180+ single_site_TDVP (state , H , sim_params , numiter_lanczos = 5 )
181+ # Check that state still has L tensors and that each tensor is a numpy array.
182+ assert state .length == L
183+ for tensor in state .tensors :
184+ assert isinstance (tensor , np .ndarray )
185+
186+ canonical_site = state .check_canonical_form ()[0 ]
187+ assert canonical_site == 0 , (
188+ f"MPS should be site-canonical at site 0 after single-site TDVP, "
189+ f"but got canonical site: { canonical_site } "
190+ )
191+
192+ def test_two_site_TDVP ():
193+ L = 5
194+ d = 2
195+ J = 1
196+ g = 0.5
197+ H = MPO ()
198+ H .init_Ising (L , d , J , g )
199+ state = MPS (L )
200+ measurements = [Observable ('z' , site ) for site in range (L )]
201+ sim_params = PhysicsSimParams (measurements , T = 0.2 , dt = 0.1 , sample_timesteps = True , N = 1 , max_bond_dim = 4 , threshold = 1e-6 , order = 1 )
202+ two_site_TDVP (state , H , sim_params , numiter_lanczos = 5 )
203+ # Check that state still has L tensors and that each tensor is a numpy array.
204+ assert state .length == L
205+ for tensor in state .tensors :
206+ assert isinstance (tensor , np .ndarray )
207+
208+ canonical_site = state .check_canonical_form ()[0 ]
209+ assert canonical_site == 0 , (
210+ f"MPS should be site-canonical at site 0 after two-site TDVP, "
211+ f"but got canonical site: { canonical_site } "
212+ )
0 commit comments