Skip to content

Commit 2182cd9

Browse files
Merge branch 'feature/hamiltonian-generation' of github.com:Stellogic/tensorcircuit-ng into ngpr22
2 parents fa5b394 + aa89735 commit 2182cd9

File tree

2 files changed

+308
-0
lines changed

2 files changed

+308
-0
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
from typing import Any, List, Tuple, Union
2+
import numpy as np
3+
from ..cons import dtypestr, backend
4+
from ..quantum import PauliStringSum2COO
5+
from .lattice import AbstractLattice
6+
7+
8+
def _create_empty_sparse_matrix(shape: Tuple[int, int]) -> Any:
9+
"""
10+
Helper function to create a backend-agnostic empty sparse matrix.
11+
"""
12+
indices = backend.convert_to_tensor(backend.zeros((0, 2), dtype="int32"))
13+
values = backend.convert_to_tensor(backend.zeros((0,), dtype=dtypestr)) # type: ignore
14+
return backend.coo_sparse_matrix(indices=indices, values=values, shape=shape) # type: ignore
15+
16+
17+
def heisenberg_hamiltonian(
18+
lattice: AbstractLattice,
19+
j_coupling: Union[float, List[float], Tuple[float, ...]] = 1.0,
20+
) -> Any:
21+
"""
22+
Generates the sparse matrix of the Heisenberg Hamiltonian for a given lattice.
23+
24+
The Heisenberg Hamiltonian is defined as:
25+
H = J * Σ_{<i,j>} (X_i X_j + Y_i Y_j + Z_i Z_j)
26+
where the sum is over all unique nearest-neighbor pairs <i,j>.
27+
28+
:param lattice: An instance of a class derived from AbstractLattice,
29+
which provides the geometric information of the system.
30+
:type lattice: AbstractLattice
31+
:param j_coupling: The coupling constants. Can be a single float for an
32+
isotropic model (Jx=Jy=Jz) or a list/tuple of 3 floats for an
33+
anisotropic model (Jx, Jy, Jz). Defaults to 1.0.
34+
:type j_coupling: Union[float, List[float], Tuple[float, ...]], optional
35+
:return: The Hamiltonian as a backend-agnostic sparse matrix.
36+
:rtype: Any
37+
"""
38+
num_sites = lattice.num_sites
39+
neighbor_pairs = lattice.get_neighbor_pairs(k=1, unique=True)
40+
41+
if isinstance(j_coupling, (float, int)):
42+
js = [float(j_coupling)] * 3
43+
else:
44+
if len(j_coupling) != 3:
45+
raise ValueError("j_coupling must be a float or a list/tuple of 3 floats.")
46+
js = [float(j) for j in j_coupling]
47+
48+
if not neighbor_pairs:
49+
return _create_empty_sparse_matrix(shape=(2**num_sites, 2**num_sites))
50+
if num_sites == 0:
51+
raise ValueError("Cannot generate a Hamiltonian for a lattice with zero sites.")
52+
53+
pauli_map = {"X": 1, "Y": 2, "Z": 3}
54+
55+
ls: List[List[int]] = []
56+
weights: List[float] = []
57+
58+
pauli_terms = ["X", "Y", "Z"]
59+
for i, j in neighbor_pairs:
60+
for idx, pauli_char in enumerate(pauli_terms):
61+
if abs(js[idx]) > 1e-9:
62+
string = [0] * num_sites
63+
string[i] = pauli_map[pauli_char]
64+
string[j] = pauli_map[pauli_char]
65+
ls.append(string)
66+
weights.append(js[idx])
67+
68+
hamiltonian_matrix = PauliStringSum2COO(ls, weight=weights, numpy=False)
69+
70+
return hamiltonian_matrix
71+
72+
73+
def rydberg_hamiltonian(
74+
lattice: AbstractLattice, omega: float, delta: float, c6: float
75+
) -> Any:
76+
"""
77+
Generates the sparse matrix of the Rydberg atom array Hamiltonian.
78+
79+
The Hamiltonian is defined as:
80+
H = Σ_i (Ω/2)X_i - Σ_i δ(1 - Z_i)/2 + Σ_{i<j} V_ij (1-Z_i)/2 (1-Z_j)/2
81+
= Σ_i (Ω/2)X_i + Σ_i (δ/2)Z_i + Σ_{i<j} (V_ij/4)(Z_iZ_j - Z_i - Z_j)
82+
where V_ij = C6 / |r_i - r_j|^6.
83+
84+
Note: Constant energy offset terms (proportional to the identity operator)
85+
are ignored in this implementation.
86+
87+
:param lattice: An instance of a class derived from AbstractLattice,
88+
which provides site coordinates and the distance matrix.
89+
:type lattice: AbstractLattice
90+
:param omega: The Rabi frequency (Ω) of the driving laser field.
91+
:type omega: float
92+
:param delta: The laser detuning (δ).
93+
:type delta: float
94+
:param c6: The Van der Waals interaction coefficient (C6).
95+
:type c6: float
96+
:return: The Hamiltonian as a backend-agnostic sparse matrix.
97+
:rtype: Any
98+
"""
99+
num_sites = lattice.num_sites
100+
if num_sites == 0:
101+
raise ValueError("Cannot generate a Hamiltonian for a lattice with zero sites.")
102+
103+
pauli_map = {"X": 1, "Y": 2, "Z": 3}
104+
ls: List[List[int]] = []
105+
weights: List[float] = []
106+
107+
for i in range(num_sites):
108+
x_string = [0] * num_sites
109+
x_string[i] = pauli_map["X"]
110+
ls.append(x_string)
111+
weights.append(omega / 2.0)
112+
113+
z_coefficients = np.zeros(num_sites)
114+
115+
for i in range(num_sites):
116+
z_coefficients[i] += delta / 2.0
117+
118+
dist_matrix = lattice.distance_matrix
119+
120+
for i in range(num_sites):
121+
for j in range(i + 1, num_sites):
122+
distance = dist_matrix[i, j]
123+
124+
if distance < 1e-9:
125+
continue
126+
127+
interaction_strength = c6 / (distance**6)
128+
coefficient = interaction_strength / 4.0
129+
130+
zz_string = [0] * num_sites
131+
zz_string[i] = pauli_map["Z"]
132+
zz_string[j] = pauli_map["Z"]
133+
ls.append(zz_string)
134+
weights.append(coefficient)
135+
136+
# The interaction term V_ij * n_i * n_j, when expanded using
137+
# n_i = (1-Z_i)/2, becomes (V_ij/4)*(I - Z_i - Z_j + Z_i*Z_j).
138+
# This contributes a positive term (+V_ij/4) to the ZZ interaction,
139+
# but negative terms (-V_ij/4) to the single-site Z_i and Z_j operators.
140+
141+
z_coefficients[i] -= coefficient
142+
z_coefficients[j] -= coefficient
143+
144+
for i in range(num_sites):
145+
if abs(z_coefficients[i]) > 1e-9:
146+
z_string = [0] * num_sites
147+
z_string[i] = pauli_map["Z"]
148+
ls.append(z_string)
149+
weights.append(z_coefficients[i]) # type: ignore
150+
151+
hamiltonian_matrix = PauliStringSum2COO(ls, weight=weights, numpy=False)
152+
153+
return hamiltonian_matrix

tests/test_hamiltonians.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import pytest
2+
import numpy as np
3+
import tensorcircuit as tc
4+
5+
6+
from tensorcircuit.templates.lattice import (
7+
ChainLattice,
8+
SquareLattice,
9+
CustomizeLattice,
10+
)
11+
from tensorcircuit.templates.hamiltonians import (
12+
heisenberg_hamiltonian,
13+
rydberg_hamiltonian,
14+
)
15+
16+
PAULI_X = np.array([[0, 1], [1, 0]], dtype=complex)
17+
PAULI_Y = np.array([[0, -1j], [1j, 0]], dtype=complex)
18+
PAULI_Z = np.array([[1, 0], [0, -1]], dtype=complex)
19+
PAULI_I = np.eye(2, dtype=complex)
20+
21+
22+
class TestHeisenbergHamiltonian:
23+
"""
24+
Test suite for the heisenberg_hamiltonian function.
25+
"""
26+
27+
def test_empty_lattice(self):
28+
"""
29+
Test that an empty lattice produces a 0x0 matrix.
30+
"""
31+
empty_lattice = CustomizeLattice(
32+
dimensionality=2, identifiers=[], coordinates=[]
33+
)
34+
h = heisenberg_hamiltonian(empty_lattice)
35+
assert h.shape == (1, 1)
36+
assert h.nnz == 0
37+
38+
def test_single_site(self):
39+
"""
40+
Test that a single-site lattice (no bonds) produces a 2x2 zero matrix.
41+
"""
42+
single_site_lattice = ChainLattice(size=(1,), pbc=False)
43+
h = heisenberg_hamiltonian(single_site_lattice)
44+
assert h.shape == (2, 2)
45+
assert h.nnz == 0
46+
47+
def test_two_sites_chain(self):
48+
"""
49+
Test a two-site chain against a manually calculated Hamiltonian.
50+
This is the most critical test for scientific correctness.
51+
"""
52+
lattice = ChainLattice(size=(2,), pbc=False)
53+
j_coupling = -1.5 # Test with a non-trivial coupling constant
54+
h_generated = heisenberg_hamiltonian(lattice, j_coupling=j_coupling)
55+
56+
# Manually construct the expected Hamiltonian: H = J * (X_0X_1 + Y_0Y_1 + Z_0Z_1)
57+
xx = np.kron(PAULI_X, PAULI_X)
58+
yy = np.kron(PAULI_Y, PAULI_Y)
59+
zz = np.kron(PAULI_Z, PAULI_Z)
60+
h_expected = j_coupling * (xx + yy + zz)
61+
62+
assert h_generated.shape == (4, 4)
63+
assert np.allclose(tc.backend.to_dense(h_generated), h_expected)
64+
65+
def test_square_lattice_properties(self):
66+
"""
67+
Test properties of a larger lattice (2x2 square) without full matrix comparison.
68+
"""
69+
lattice = SquareLattice(size=(2, 2), pbc=True) # 4 sites, 8 bonds with PBC
70+
h = heisenberg_hamiltonian(lattice, j_coupling=1.0)
71+
72+
assert h.shape == (16, 16)
73+
assert h.nnz > 0
74+
h_dense = tc.backend.to_dense(h)
75+
assert np.allclose(h_dense, h_dense.conj().T)
76+
77+
78+
class TestRydbergHamiltonian:
79+
"""
80+
Test suite for the rydberg_hamiltonian function.
81+
"""
82+
83+
def test_single_site_rydberg(self):
84+
"""
85+
Test a single atom, which should only have driving and detuning terms.
86+
"""
87+
lattice = ChainLattice(size=(1,), pbc=False)
88+
omega, delta, c6 = 2.0, 0.5, 100.0
89+
h_generated = rydberg_hamiltonian(lattice, omega, delta, c6)
90+
91+
h_expected = (omega / 2.0) * PAULI_X + (delta / 2.0) * PAULI_Z
92+
93+
assert h_generated.shape == (2, 2)
94+
assert np.allclose(tc.backend.to_dense(h_generated), h_expected)
95+
96+
def test_two_sites_rydberg(self):
97+
"""
98+
Test a two-site chain for Rydberg Hamiltonian, including interaction.
99+
"""
100+
lattice = ChainLattice(size=(2,), pbc=False, lattice_constant=1.5)
101+
omega, delta, c6 = 1.0, -0.5, 10.0
102+
h_generated = rydberg_hamiltonian(lattice, omega, delta, c6)
103+
104+
v_ij = c6 / (1.5**6)
105+
106+
h1 = (omega / 2.0) * (np.kron(PAULI_X, PAULI_I) + np.kron(PAULI_I, PAULI_X))
107+
z0_coeff = delta / 2.0 - v_ij / 4.0
108+
z1_coeff = delta / 2.0 - v_ij / 4.0
109+
h2 = z0_coeff * np.kron(PAULI_Z, PAULI_I) + z1_coeff * np.kron(PAULI_I, PAULI_Z)
110+
h3 = (v_ij / 4.0) * np.kron(PAULI_Z, PAULI_Z)
111+
112+
h_expected = h1 + h2 + h3
113+
114+
assert h_generated.shape == (4, 4)
115+
h_generated_dense = tc.backend.to_dense(h_generated)
116+
117+
assert np.allclose(h_generated_dense, h_expected)
118+
119+
def test_zero_distance_robustness(self):
120+
"""
121+
Test that the function does not crash when two atoms have zero distance.
122+
"""
123+
lattice = CustomizeLattice(
124+
dimensionality=2,
125+
identifiers=[0, 1],
126+
coordinates=[[0.0, 0.0], [0.0, 0.0]],
127+
)
128+
129+
try:
130+
h = rydberg_hamiltonian(lattice, omega=1.0, delta=1.0, c6=1.0)
131+
# The X terms contribute 8 non-zero elements.
132+
# The Z terms (Z0+Z1) have diagonal elements that cancel out,
133+
# resulting in only 2 non-zero elements. Total nnz = 8 + 2 = 10.
134+
assert h.nnz == 10
135+
except ZeroDivisionError:
136+
pytest.fail("The function failed to handle zero distance between sites.")
137+
138+
def test_anisotropic_heisenberg(self):
139+
"""
140+
Test the anisotropic Heisenberg model with different Jx, Jy, Jz.
141+
"""
142+
lattice = ChainLattice(size=(2,), pbc=False)
143+
j_coupling = [-1.0, 0.5, 2.0] # Jx, Jy, Jz
144+
h_generated = heisenberg_hamiltonian(lattice, j_coupling=j_coupling)
145+
146+
# Manually construct the expected Hamiltonian
147+
jx, jy, jz = j_coupling
148+
xx = np.kron(PAULI_X, PAULI_X)
149+
yy = np.kron(PAULI_Y, PAULI_Y)
150+
zz = np.kron(PAULI_Z, PAULI_Z)
151+
h_expected = jx * xx + jy * yy + jz * zz
152+
153+
h_generated_dense = tc.backend.to_dense(h_generated)
154+
assert h_generated_dense.shape == (4, 4)
155+
assert np.allclose(h_generated_dense, h_expected)

0 commit comments

Comments
 (0)