Skip to content

Commit d0b1b15

Browse files
added new tests
1 parent 8a86893 commit d0b1b15

File tree

3 files changed

+561
-0
lines changed

3 files changed

+561
-0
lines changed

tests/test_PhysicsTJM.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import pytest
2+
from unittest.mock import patch
3+
4+
from yaqs.core.data_structures.networks import MPO, MPS
5+
from yaqs.core.data_structures.noise_model import NoiseModel
6+
from yaqs.core.data_structures.simulation_parameters import Observable, PhysicsSimParams
7+
from yaqs.physics.PhysicsTJM import initialize, step_through, sample, PhysicsTJM_2, PhysicsTJM_1
8+
9+
10+
def test_initialize():
11+
L = 5
12+
d = 2
13+
J = 1
14+
g = 0.5
15+
H = MPO()
16+
H.init_Ising(L, d, J, g)
17+
state = MPS(L)
18+
noise_model = NoiseModel(['relaxation'], [0.1])
19+
measurements = [Observable('x', site) for site in range(L)]
20+
sim_params = PhysicsSimParams(measurements, T=0.2, dt=0.2, sample_timesteps=False, N=1, max_bond_dim=2, threshold=1e-6, order=1)
21+
with patch('yaqs.physics.PhysicsTJM.apply_dissipation') as mock_dissipation, \
22+
patch('yaqs.physics.PhysicsTJM.stochastic_process') as mock_stochastic_process:
23+
initialize(state, noise_model, sim_params)
24+
mock_dissipation.assert_called_once_with(state, noise_model, sim_params.dt/2)
25+
mock_stochastic_process.assert_called_once_with(state, noise_model, sim_params.dt)
26+
27+
def test_step_through():
28+
L = 5
29+
d = 2
30+
J = 1
31+
g = 0.5
32+
H = MPO()
33+
H.init_Ising(L, d, J, g)
34+
state = MPS(L)
35+
noise_model = NoiseModel(['relaxation'], [0.1])
36+
measurements = [Observable('x', site) for site in range(L)]
37+
sim_params = PhysicsSimParams(measurements, T=0.2, dt=0.2, sample_timesteps=False, N=1, max_bond_dim=2, threshold=1e-6, order=1)
38+
with patch('yaqs.physics.PhysicsTJM.dynamic_TDVP') as mock_dynamic_TDVP, \
39+
patch('yaqs.physics.PhysicsTJM.apply_dissipation') as mock_dissipation, \
40+
patch('yaqs.physics.PhysicsTJM.stochastic_process') as mock_stochastic_process:
41+
step_through(state, H, noise_model, sim_params)
42+
mock_dynamic_TDVP(state, H, sim_params)
43+
mock_dissipation.assert_called_once_with(state, noise_model, sim_params.dt)
44+
mock_stochastic_process.assert_called_once_with(state, noise_model, sim_params.dt)
45+
46+
def test_PhysicsTJM_2():
47+
L = 5
48+
d = 2
49+
J = 1
50+
g = 0.5
51+
H = MPO()
52+
H.init_Ising(L, d, J, g)
53+
state = MPS(L)
54+
noise_model = None
55+
measurements = [Observable('z', site) for site in range(L)]
56+
sim_params = PhysicsSimParams(measurements, T=0.2, dt=0.1, sample_timesteps=False, N=1, max_bond_dim=4, threshold=1e-6, order=2)
57+
args = (0, state, noise_model, sim_params, H)
58+
results = PhysicsTJM_2(args)
59+
# When sample_timesteps is True, results should have shape (num_observables, len(times))
60+
assert results.shape == (len(measurements), 1), "Results incorrect shape"
61+
62+
def test_PhysicsTJM_2_sample_timesteps():
63+
L = 5
64+
d = 2
65+
J = 1
66+
g = 0.5
67+
H = MPO()
68+
H.init_Ising(L, d, J, g)
69+
state = MPS(L)
70+
noise_model = None
71+
measurements = [Observable('z', site) for site in range(L)]
72+
sim_params = PhysicsSimParams(measurements, T=0.2, dt=0.1, sample_timesteps=True, N=1, max_bond_dim=4, threshold=1e-6, order=2)
73+
args = (0, state, noise_model, sim_params, H)
74+
results = PhysicsTJM_2(args)
75+
# When sample_timesteps is True, results should have shape (num_observables, len(times))
76+
assert results.shape == (len(measurements), len(sim_params.times)), "Results incorrect shape"
77+
78+
def test_PhysicsTJM_1():
79+
L = 5
80+
d = 2
81+
J = 1
82+
g = 0.5
83+
H = MPO()
84+
H.init_Ising(L, d, J, g)
85+
state = MPS(L)
86+
noise_model = None
87+
measurements = [Observable('z', site) for site in range(L)]
88+
sim_params = PhysicsSimParams(measurements, T=0.2, dt=0.1, sample_timesteps=False, N=1, max_bond_dim=4, threshold=1e-6, order=1)
89+
args = (0, state, noise_model, sim_params, H)
90+
results = PhysicsTJM_1(args)
91+
# When sample_timesteps is True, results should have shape (num_observables, len(times))
92+
assert results.shape == (len(measurements), 1), "Results incorrect shape"
93+
94+
def test_PhysicsTJM_1_sample_timesteps():
95+
L = 5
96+
d = 2
97+
J = 1
98+
g = 0.5
99+
H = MPO()
100+
H.init_Ising(L, d, J, g)
101+
state = MPS(L)
102+
noise_model = None
103+
measurements = [Observable('z', site) for site in range(L)]
104+
sim_params = PhysicsSimParams(measurements, T=0.2, dt=0.1, sample_timesteps=True, N=1, max_bond_dim=4, threshold=1e-6, order=1)
105+
args = (0, state, noise_model, sim_params, H)
106+
results = PhysicsTJM_1(args)
107+
# When sample_timesteps is True, results should have shape (num_observables, len(times))
108+
assert results.shape == (len(measurements), len(sim_params.times)), "Results incorrect shape"

tests/test_TDVP.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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

Comments
 (0)