Skip to content

Commit e8b1283

Browse files
committed
refactor(templates): Improve hamiltonian functions based on review
This commit refactors the hamiltonian generation functions to be more robust, flexible, and aligned with the project's backend-agnostic architecture, based on feedback from the recent code review. Key changes include: - **Backend Agnosticism:** The `PauliStringSum2COO` call now uses `numpy=False`, ensuring the returned sparse matrix is a native tensor of the currently active backend (TensorFlow, JAX, etc.). The return type hints have been updated to `Any` to reflect this. - **Anisotropic Heisenberg Model:** The `heisenberg_hamiltonian` function now accepts a list or tuple for `j_coupling`, allowing for the definition of anisotropic models with different Jx, Jy, and Jz values. - **Test Suite Enhancement:** Tests have been updated to use `tc.backend.to_dense` for backend-agnostic validation. A new test case for the anisotropic Heisenberg model has also been added to ensure its correctness. - **Code Cleanup:** A redundant `float()` cast was removed from the Rydberg hamiltonian logic.
1 parent 7681a98 commit e8b1283

File tree

2 files changed

+88
-57
lines changed

2 files changed

+88
-57
lines changed

tensorcircuit/templates/hamiltonians.py

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
import typing
2-
from typing import cast
3-
from scipy.sparse import coo_matrix
2+
from typing import Any, List, Tuple, Union
43
import numpy as np
4+
from tensorcircuit.cons import dtypestr, backend
55
import tensorcircuit as tc
66
from .lattice import AbstractLattice
77

88

9-
def generate_heisenberg_hamiltonian(
10-
lattice: AbstractLattice, j_coupling: float = 1.0
11-
) -> coo_matrix:
9+
def _create_empty_sparse_matrix(shape: Tuple[int, int]) -> Any:
10+
"""
11+
Helper function to create a backend-agnostic empty sparse matrix.
12+
"""
13+
indices = tc.backend.convert_to_tensor(backend.zeros((0, 2), dtype="int32"))
14+
values = tc.backend.convert_to_tensor(backend.zeros((0,), dtype=dtypestr)) # type: ignore
15+
return tc.backend.coo_sparse_matrix(indices=indices, values=values, shape=shape) # type: ignore
16+
17+
18+
def heisenberg_hamiltonian(
19+
lattice: AbstractLattice,
20+
j_coupling: Union[float, List[float], Tuple[float, ...]] = 1.0,
21+
) -> Any:
1222
"""
1323
Generates the sparse matrix of the Heisenberg Hamiltonian for a given lattice.
1424
@@ -19,51 +29,49 @@ def generate_heisenberg_hamiltonian(
1929
:param lattice: An instance of a class derived from AbstractLattice,
2030
which provides the geometric information of the system.
2131
:type lattice: AbstractLattice
22-
:param j_coupling: The coupling constant for the Heisenberg interaction. Defaults to 1.0.
23-
:type j_coupling: float, optional
24-
:return: The Hamiltonian represented as a SciPy COO sparse matrix.
25-
:rtype: coo_matrix
32+
:param j_coupling: The coupling constants. Can be a single float for an
33+
isotropic model (Jx=Jy=Jz) or a list/tuple of 3 floats for an
34+
anisotropic model (Jx, Jy, Jz). Defaults to 1.0.
35+
:type j_coupling: Union[float, List[float], Tuple[float, ...]], optional
36+
:return: The Hamiltonian as a backend-agnostic sparse matrix.
37+
:rtype: Any
2638
"""
2739
num_sites = lattice.num_sites
28-
if num_sites == 0:
29-
return coo_matrix((0, 0))
30-
3140
neighbor_pairs = lattice.get_neighbor_pairs(k=1, unique=True)
32-
if not neighbor_pairs:
33-
return coo_matrix((2**num_sites, 2**num_sites))
41+
42+
if isinstance(j_coupling, (float, int)):
43+
js = [float(j_coupling)] * 3
44+
else:
45+
if len(j_coupling) != 3:
46+
raise ValueError("j_coupling must be a float or a list/tuple of 3 floats.")
47+
js = [float(j) for j in j_coupling]
48+
49+
if num_sites == 0 or not neighbor_pairs:
50+
return _create_empty_sparse_matrix(shape=(2**num_sites, 2**num_sites))
3451

3552
pauli_map = {"X": 1, "Y": 2, "Z": 3}
3653

3754
ls: typing.List[typing.List[int]] = []
3855
weights: typing.List[float] = []
3956

57+
pauli_terms = ["X", "Y", "Z"]
4058
for i, j in neighbor_pairs:
41-
xx_string = [0] * num_sites
42-
xx_string[i] = pauli_map["X"]
43-
xx_string[j] = pauli_map["X"]
44-
ls.append(xx_string)
45-
weights.append(j_coupling)
46-
47-
yy_string = [0] * num_sites
48-
yy_string[i] = pauli_map["Y"]
49-
yy_string[j] = pauli_map["Y"]
50-
ls.append(yy_string)
51-
weights.append(j_coupling)
52-
53-
zz_string = [0] * num_sites
54-
zz_string[i] = pauli_map["Z"]
55-
zz_string[j] = pauli_map["Z"]
56-
ls.append(zz_string)
57-
weights.append(j_coupling)
59+
for idx, pauli_char in enumerate(pauli_terms):
60+
if abs(js[idx]) > 1e-9:
61+
string = [0] * num_sites
62+
string[i] = pauli_map[pauli_char]
63+
string[j] = pauli_map[pauli_char]
64+
ls.append(string)
65+
weights.append(js[idx])
5866

59-
hamiltonian_matrix = tc.quantum.PauliStringSum2COO(ls, weight=weights, numpy=True)
67+
hamiltonian_matrix = tc.quantum.PauliStringSum2COO(ls, weight=weights, numpy=False)
6068

61-
return cast(coo_matrix, hamiltonian_matrix)
69+
return hamiltonian_matrix
6270

6371

64-
def generate_rydberg_hamiltonian(
72+
def rydberg_hamiltonian(
6573
lattice: AbstractLattice, omega: float, delta: float, c6: float
66-
) -> coo_matrix:
74+
) -> Any:
6775
"""
6876
Generates the sparse matrix of the Rydberg atom array Hamiltonian.
6977
@@ -84,12 +92,12 @@ def generate_rydberg_hamiltonian(
8492
:type delta: float
8593
:param c6: The Van der Waals interaction coefficient (C6).
8694
:type c6: float
87-
:return: The Hamiltonian represented as a SciPy COO sparse matrix.
88-
:rtype: coo_matrix
95+
:return: The Hamiltonian as a backend-agnostic sparse matrix.
96+
:rtype: Any
8997
"""
9098
num_sites = lattice.num_sites
9199
if num_sites == 0:
92-
return coo_matrix((0, 0))
100+
return _create_empty_sparse_matrix(shape=(1, 1))
93101

94102
pauli_map = {"X": 1, "Y": 2, "Z": 3}
95103
ls: typing.List[typing.List[int]] = []
@@ -132,8 +140,8 @@ def generate_rydberg_hamiltonian(
132140
z_string = [0] * num_sites
133141
z_string[i] = pauli_map["Z"]
134142
ls.append(z_string)
135-
weights.append(float(z_coefficients[i]))
143+
weights.append(z_coefficients[i]) # type: ignore
136144

137-
hamiltonian_matrix = tc.quantum.PauliStringSum2COO(ls, weight=weights, numpy=True)
145+
hamiltonian_matrix = tc.quantum.PauliStringSum2COO(ls, weight=weights, numpy=False)
138146

139-
return cast(coo_matrix, hamiltonian_matrix)
147+
return hamiltonian_matrix

tests/test_hamiltonians.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import pytest
22
import numpy as np
3+
import tensorcircuit as tc
4+
35

46
from tensorcircuit.templates.lattice import (
57
ChainLattice,
68
SquareLattice,
79
CustomizeLattice,
810
)
911
from tensorcircuit.templates.hamiltonians import (
10-
generate_heisenberg_hamiltonian,
11-
generate_rydberg_hamiltonian,
12+
heisenberg_hamiltonian,
13+
rydberg_hamiltonian,
1214
)
1315

1416
PAULI_X = np.array([[0, 1], [1, 0]], dtype=complex)
@@ -19,7 +21,7 @@
1921

2022
class TestHeisenbergHamiltonian:
2123
"""
22-
Test suite for the generate_heisenberg_hamiltonian function.
24+
Test suite for the heisenberg_hamiltonian function.
2325
"""
2426

2527
def test_empty_lattice(self):
@@ -29,16 +31,16 @@ def test_empty_lattice(self):
2931
empty_lattice = CustomizeLattice(
3032
dimensionality=2, identifiers=[], coordinates=[]
3133
)
32-
h = generate_heisenberg_hamiltonian(empty_lattice)
33-
assert h.shape == (0, 0)
34+
h = heisenberg_hamiltonian(empty_lattice)
35+
assert h.shape == (1, 1)
3436
assert h.nnz == 0
3537

3638
def test_single_site(self):
3739
"""
3840
Test that a single-site lattice (no bonds) produces a 2x2 zero matrix.
3941
"""
4042
single_site_lattice = ChainLattice(size=(1,), pbc=False)
41-
h = generate_heisenberg_hamiltonian(single_site_lattice)
43+
h = heisenberg_hamiltonian(single_site_lattice)
4244
assert h.shape == (2, 2)
4345
assert h.nnz == 0
4446

@@ -49,7 +51,7 @@ def test_two_sites_chain(self):
4951
"""
5052
lattice = ChainLattice(size=(2,), pbc=False)
5153
j_coupling = -1.5 # Test with a non-trivial coupling constant
52-
h_generated = generate_heisenberg_hamiltonian(lattice, j_coupling=j_coupling)
54+
h_generated = heisenberg_hamiltonian(lattice, j_coupling=j_coupling)
5355

5456
# Manually construct the expected Hamiltonian: H = J * (X_0X_1 + Y_0Y_1 + Z_0Z_1)
5557
xx = np.kron(PAULI_X, PAULI_X)
@@ -58,24 +60,24 @@ def test_two_sites_chain(self):
5860
h_expected = j_coupling * (xx + yy + zz)
5961

6062
assert h_generated.shape == (4, 4)
61-
assert np.allclose(h_generated.toarray(), h_expected)
63+
assert np.allclose(tc.backend.to_dense(h_generated), h_expected)
6264

6365
def test_square_lattice_properties(self):
6466
"""
6567
Test properties of a larger lattice (2x2 square) without full matrix comparison.
6668
"""
6769
lattice = SquareLattice(size=(2, 2), pbc=True) # 4 sites, 8 bonds with PBC
68-
h = generate_heisenberg_hamiltonian(lattice, j_coupling=1.0)
70+
h = heisenberg_hamiltonian(lattice, j_coupling=1.0)
6971

7072
assert h.shape == (16, 16)
7173
assert h.nnz > 0
72-
h_dense = h.toarray()
74+
h_dense = tc.backend.to_dense(h)
7375
assert np.allclose(h_dense, h_dense.conj().T)
7476

7577

7678
class TestRydbergHamiltonian:
7779
"""
78-
Test suite for the generate_rydberg_hamiltonian function.
80+
Test suite for the rydberg_hamiltonian function.
7981
"""
8082

8183
def test_single_site_rydberg(self):
@@ -84,20 +86,20 @@ def test_single_site_rydberg(self):
8486
"""
8587
lattice = ChainLattice(size=(1,), pbc=False)
8688
omega, delta, c6 = 2.0, 0.5, 100.0
87-
h_generated = generate_rydberg_hamiltonian(lattice, omega, delta, c6)
89+
h_generated = rydberg_hamiltonian(lattice, omega, delta, c6)
8890

8991
h_expected = (omega / 2.0) * PAULI_X + (delta / 2.0) * PAULI_Z
9092

9193
assert h_generated.shape == (2, 2)
92-
assert np.allclose(h_generated.toarray(), h_expected)
94+
assert np.allclose(tc.backend.to_dense(h_generated), h_expected)
9395

9496
def test_two_sites_rydberg(self):
9597
"""
9698
Test a two-site chain for Rydberg Hamiltonian, including interaction.
9799
"""
98100
lattice = ChainLattice(size=(2,), pbc=False, lattice_constant=1.5)
99101
omega, delta, c6 = 1.0, -0.5, 10.0
100-
h_generated = generate_rydberg_hamiltonian(lattice, omega, delta, c6)
102+
h_generated = rydberg_hamiltonian(lattice, omega, delta, c6)
101103

102104
v_ij = c6 / (1.5**6)
103105

@@ -110,7 +112,9 @@ def test_two_sites_rydberg(self):
110112
h_expected = h1 + h2 + h3
111113

112114
assert h_generated.shape == (4, 4)
113-
assert np.allclose(h_generated.toarray(), h_expected)
115+
h_generated_dense = tc.backend.to_dense(h_generated)
116+
117+
assert np.allclose(h_generated_dense, h_expected)
114118

115119
def test_zero_distance_robustness(self):
116120
"""
@@ -123,10 +127,29 @@ def test_zero_distance_robustness(self):
123127
)
124128

125129
try:
126-
h = generate_rydberg_hamiltonian(lattice, omega=1.0, delta=1.0, c6=1.0)
130+
h = rydberg_hamiltonian(lattice, omega=1.0, delta=1.0, c6=1.0)
127131
# The X terms contribute 8 non-zero elements.
128132
# The Z terms (Z0+Z1) have diagonal elements that cancel out,
129133
# resulting in only 2 non-zero elements. Total nnz = 8 + 2 = 10.
130134
assert h.nnz == 10
131135
except ZeroDivisionError:
132136
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)