Skip to content

Commit 343de43

Browse files
authored
Merge pull request #36 from simsaidan/aidanfinishtests
expand out the test suite
2 parents c408a01 + 8f3289a commit 343de43

14 files changed

Lines changed: 696 additions & 5 deletions

.coveragerc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ branch = True
66
[report]
77
show_missing = True
88
precision = 2
9-
# Fail the coverage run if total coverage drops below this floor. Adjust upward toward
10-
# the codecov target (70..90) as coverage allows (currently ~78%).
11-
fail_under = 75
9+
# Fail the coverage run if total coverage drops below this floor.
10+
fail_under = 90
1211
omit =
1312
*/__init__.py
1413
*/test/*

shadowsim/core/hamiltonian.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def to_hamiltonian_set(self):
4848

4949
def to_local_hamiltonian(self, local_dim: int = 2):
5050
"""Returns a LocalHamiltonian representing the Hamiltonian acting on
51-
the given sites.
51+
a contiguous block of sites starting at 0.
5252
5353
Parameter local_dim: The local dimension of the Hamiltonian.
5454
Precondition: local_dim is a positive integer.
@@ -58,5 +58,16 @@ def to_local_hamiltonian(self, local_dim: int = 2):
5858

5959
from shadowsim.core.local_hamiltonian import LocalHamiltonian
6060

61-
lo = self.to_local_operator(local_dim)
61+
dim = int(self.matrix.shape[0])
62+
n_sites = 0
63+
span = 1
64+
while span < dim:
65+
span *= local_dim
66+
n_sites += 1
67+
if span != dim:
68+
raise ValueError(
69+
f"Hamiltonian dimension {dim} is not a power of local_dim={local_dim}"
70+
)
71+
sites = list(range(n_sites))
72+
lo = self.to_local_operator(sites, local_dim)
6273
return LocalHamiltonian(lo.matrix, lo.sites, lo.local_dim)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
from pathlib import Path
2+
3+
import numpy as np
4+
import pytest
5+
6+
from shadowsim.benchmarking import Benchmark
7+
from shadowsim.core import Hamiltonian
8+
from shadowsim.core import State
9+
from shadowsim.simulators.simulator import Simulator
10+
11+
12+
Z = np.array([[1, 0], [0, -1]], dtype=np.complex128)
13+
14+
15+
class StubSimulator(Simulator):
16+
"""Minimal simulator with canned expectation traces."""
17+
18+
def __init__(
19+
self,
20+
*,
21+
total_time: float = 1.0,
22+
time_steps: int = 3,
23+
curves: list[np.ndarray] | None = None,
24+
id: str = "stub",
25+
):
26+
super().__init__(
27+
[Hamiltonian(Z)],
28+
[],
29+
State(np.array([1.0, 0.0], dtype=np.complex128), 1),
30+
1,
31+
total_time,
32+
time_steps,
33+
id,
34+
)
35+
if curves is None:
36+
t = self.tlist
37+
curves = [np.cos(t), np.sin(t)]
38+
self._curves = [np.asarray(c, dtype=float) for c in curves]
39+
40+
def simulate(self):
41+
self.results = list(self._curves)
42+
return self.results
43+
44+
45+
def _pair(**kwargs):
46+
return StubSimulator(id="a", **kwargs), StubSimulator(id="b", **kwargs)
47+
48+
49+
def test_benchmark_rejects_mismatched_tlist():
50+
a = StubSimulator(time_steps=3)
51+
b = StubSimulator(time_steps=4)
52+
with pytest.raises(ValueError, match="same time grid"):
53+
Benchmark(a, b)
54+
55+
56+
def test_benchmark_run_and_get_results():
57+
a, b = _pair()
58+
benchmark = Benchmark(a, b)
59+
60+
assert np.array_equal(benchmark.get_tlist(), a.tlist)
61+
62+
benchmark.run()
63+
all_a, all_b = benchmark.get_results()
64+
assert len(all_a) == len(all_b) == 2
65+
assert np.allclose(all_a[0], a.get_results(0))
66+
assert np.allclose(all_b[1], b.get_results(1))
67+
68+
first_a, first_b = benchmark.get_results(0)
69+
assert np.allclose(first_a, a.get_results(0))
70+
assert np.allclose(first_b, b.get_results(0))
71+
72+
73+
def test_benchmark_save_result_plot_writes_three_files(tmp_path, monkeypatch):
74+
monkeypatch.chdir(tmp_path)
75+
a, b = _pair()
76+
# Distinct curves so the abs-diff plot is nontrivial.
77+
b._curves = [np.cos(b.tlist) + 0.1, np.sin(b.tlist) - 0.1]
78+
benchmark = Benchmark(a, b)
79+
benchmark.run()
80+
81+
pa, pb, pdiff = benchmark.save_result_plot(
82+
labels=["cavity", "emitter"],
83+
title_a="A",
84+
title_b="B",
85+
title="diff",
86+
dpi=80,
87+
)
88+
89+
assert pa.is_file()
90+
assert pb.is_file()
91+
assert pdiff.is_file()
92+
assert pa.parent == Path("results")
93+
assert pdiff.name.startswith("benchmark_abs_diff_")
94+
95+
96+
def test_benchmark_abs_diff_requires_results():
97+
a, b = _pair()
98+
benchmark = Benchmark(a, b)
99+
with pytest.raises(ValueError, match="Results are not available"):
100+
benchmark._save_abs_diff_plot(indices=None, dpi=80, title=None)
101+
102+
103+
def test_benchmark_abs_diff_with_explicit_indices_and_no_title(tmp_path, monkeypatch):
104+
monkeypatch.chdir(tmp_path)
105+
a, b = _pair()
106+
a.results = a._curves
107+
b.results = b._curves
108+
benchmark = Benchmark(a, b)
109+
110+
path = benchmark._save_abs_diff_plot(indices=[0], dpi=80, title=None)
111+
assert path.is_file()
112+
113+
114+
def test_benchmark_abs_diff_rejects_mismatched_observable_counts():
115+
a = StubSimulator(id="a", curves=[np.zeros(3), np.ones(3)])
116+
b = StubSimulator(id="b", curves=[np.zeros(3)])
117+
a.results = a._curves
118+
b.results = b._curves
119+
benchmark = Benchmark(a, b)
120+
121+
with pytest.raises(ValueError, match="Mismatched observable counts"):
122+
benchmark._save_abs_diff_plot(indices=None, dpi=80, title=None)
123+
124+
125+
def test_benchmark_str_and_repr():
126+
a, b = _pair()
127+
benchmark = Benchmark(a, b)
128+
129+
assert str(benchmark) == "Benchmark(simulator_a=StubSimulator, simulator_b=StubSimulator)"
130+
assert "StubSimulator(" in repr(benchmark)

test/core/test_combined_hamiltonian_matrix.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,19 @@ def test_combined_hamiltonian_matrix_rejects_mixed_local_dims():
2121
h3 = LocalHamiltonian(np.diag([1.0, 0.0, -1.0]), sites=[0], local_dim=3)
2222
with pytest.raises(ValueError, match="mixed local_dim"):
2323
combined_hamiltonian_matrix([h2, h3], num_qubits=1)
24+
25+
26+
def test_combined_hamiltonian_matrix_embeds_local_with_identity_padding():
27+
# Local on middle qubit of a 3-qubit chain → n_before > 0 and n_after > 0.
28+
local = LocalHamiltonian(np.diag([1.0, -1.0]), sites=[1], local_dim=2)
29+
out = combined_hamiltonian_matrix([local], num_qubits=3)
30+
expected = np.kron(np.eye(2), np.kron(np.diag([1.0, -1.0]), np.eye(2)))
31+
assert out.shape == (8, 8)
32+
assert np.allclose(out, expected)
33+
34+
35+
def test_combined_hamiltonian_matrix_rejects_disagreeing_dimensions():
36+
local = LocalHamiltonian(np.diag([1.0, -1.0]), sites=[0], local_dim=2)
37+
full = Hamiltonian(np.eye(4, dtype=np.complex128))
38+
with pytest.raises(ValueError, match="dimensions disagree"):
39+
combined_hamiltonian_matrix([local, full], num_qubits=1)

test/core/test_hamiltonian.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import numpy as np
2+
import pytest
3+
4+
from shadowsim.core import Hamiltonian
5+
from shadowsim.core import HamiltonianSet
6+
from shadowsim.core import LocalHamiltonian
7+
8+
9+
Z = np.diag([1.0, -1.0]).astype(np.complex128)
10+
X = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=np.complex128)
11+
12+
13+
def test_hamiltonian_str_repr_equality_and_conversions():
14+
h = Hamiltonian(Z)
15+
other = Hamiltonian(Z)
16+
different = Hamiltonian(X)
17+
18+
assert "Hamiltonian(matrix=" in str(h)
19+
assert "Hamiltonian(matrix=" in repr(h)
20+
assert h == other
21+
assert h != different
22+
23+
h_set = h.to_hamiltonian_set()
24+
assert isinstance(h_set, HamiltonianSet)
25+
assert len(h_set) == 1
26+
assert h_set[0] == h
27+
28+
local = h.to_local_hamiltonian(local_dim=2)
29+
assert isinstance(local, LocalHamiltonian)
30+
assert local.sites == [0]
31+
assert local.local_dim == 2
32+
assert np.allclose(local.matrix, Z)
33+
34+
35+
def test_hamiltonian_to_local_hamiltonian_validates_local_dim():
36+
h = Hamiltonian(Z)
37+
with pytest.raises(AssertionError, match="local_dim must be an integer"):
38+
h.to_local_hamiltonian(local_dim=2.0) # type: ignore[arg-type]
39+
with pytest.raises(AssertionError, match="local_dim must be a positive integer"):
40+
h.to_local_hamiltonian(local_dim=0)
41+
with pytest.raises(ValueError, match="not a power of local_dim"):
42+
Hamiltonian(np.eye(3, dtype=np.complex128)).to_local_hamiltonian(local_dim=2)

test/core/test_hamiltonian_set.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import numpy as np
2+
import pytest
3+
4+
from shadowsim.core import Hamiltonian
5+
from shadowsim.core import HamiltonianSet
6+
7+
8+
Z = np.diag([1.0, -1.0]).astype(np.complex128)
9+
X = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=np.complex128)
10+
11+
12+
def test_hamiltonian_set_protocols_and_repr():
13+
h1 = Hamiltonian(Z)
14+
h2 = Hamiltonian(X)
15+
h_set = HamiltonianSet([h1, h2])
16+
17+
assert h_set.hamiltonian_count == 2
18+
assert len(h_set) == 2
19+
assert list(h_set) == [h1, h2]
20+
assert h_set[0] == h1
21+
assert h1 in h_set
22+
assert Hamiltonian(np.eye(2, dtype=np.complex128)) not in h_set
23+
assert "HamiltonianSet(hamiltonian_count=2)" == str(h_set)
24+
assert "HamiltonianSet(hamiltonians=" in repr(h_set)
25+
26+
27+
def test_hamiltonian_set_validates_inputs():
28+
with pytest.raises(AssertionError, match="hamiltonians must be a list"):
29+
HamiltonianSet(Hamiltonian(Z)) # type: ignore[arg-type]
30+
with pytest.raises(AssertionError, match="all hamiltonians must be Hamiltonian"):
31+
HamiltonianSet([Hamiltonian(Z), "nope"]) # type: ignore[list-item]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import numpy as np
2+
import pytest
3+
4+
from shadowsim.core import Hamiltonian
5+
from shadowsim.core import LocalHamiltonian
6+
7+
8+
Z = np.diag([1.0, -1.0]).astype(np.complex128)
9+
10+
11+
def test_local_hamiltonian_to_full_and_accessors():
12+
local = LocalHamiltonian(Z, sites=[1], local_dim=2)
13+
full = local.to_full_hamiltonian(total_sites=3)
14+
15+
assert isinstance(full, Hamiltonian)
16+
expected = np.kron(np.eye(2), np.kron(Z, np.eye(2)))
17+
assert np.allclose(full.matrix, expected)
18+
assert local.get_sites() == [1]
19+
assert local.get_local_dim() == 2
20+
assert np.allclose(local.get_matrix(), Z)
21+
22+
23+
def test_local_hamiltonian_to_full_validates_total_sites():
24+
local = LocalHamiltonian(Z, sites=[1], local_dim=2)
25+
with pytest.raises(AssertionError, match="total_sites must be an integer"):
26+
local.to_full_hamiltonian(3.0) # type: ignore[arg-type]
27+
with pytest.raises(AssertionError, match="total_sites must be positive"):
28+
local.to_full_hamiltonian(0)
29+
with pytest.raises(ValueError, match="too small"):
30+
local.to_full_hamiltonian(1)
31+
32+
33+
def test_local_hamiltonian_str_and_repr():
34+
local = LocalHamiltonian(Z, sites=[0], local_dim=2)
35+
text = str(local)
36+
rep = repr(local)
37+
assert "LocalHamiltonian(" in text
38+
assert "sites=[0]" in text
39+
assert "LocalHamiltonian(" in rep
40+
assert "local_dim=2" in rep

test/core/test_local_operator.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,24 @@ def test_local_operator_to_full_operator_validates_total_sites():
5050
local.to_full_operator(0)
5151
with pytest.raises(ValueError, match="too small"):
5252
local.to_full_operator(2)
53+
54+
55+
def test_local_operator_accessors_and_conversions():
56+
local = LocalOperator(np.eye(2), sites=[0], local_dim=2)
57+
assert local.get_sites() == [0]
58+
assert local.get_local_dim() == 2
59+
assert np.allclose(local.get_matrix(), np.eye(2))
60+
as_op = local.to_operator()
61+
assert isinstance(as_op, Operator)
62+
assert as_op == Operator(np.eye(2))
63+
64+
65+
def test_local_operator_str_and_repr():
66+
local = LocalOperator(np.eye(2), sites=[0], local_dim=2)
67+
text = str(local)
68+
rep = repr(local)
69+
assert "LocalOperator(" in text
70+
assert "sites=[0]" in text
71+
assert "LocalOperator(" in rep
72+
assert "local_dim=2" in rep
73+

test/core/test_operator.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ def test_operator_equality_and_inequality():
4747
assert a != c
4848

4949

50+
def test_operator_str_and_repr():
51+
op = Operator(np.eye(2, dtype=np.complex128))
52+
assert "Operator(matrix=" in str(op)
53+
assert "Operator(matrix=" in repr(op)
54+
55+
5056
def test_operator_set_name_updates_name_field():
5157
op = Operator(np.eye(2))
5258
op.set_name("X")

test/core/test_pauli.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,25 @@ def test_pauli_multiply_and_commutator():
3838

3939
def test_pauli_commutator_returns_none_for_commuting_pair():
4040
assert Pauli("X").commutator(Pauli("X")) == (None, None)
41+
42+
43+
def test_pauli_eq_returns_notimplemented_for_other_types():
44+
assert Pauli("X").__eq__("X") is NotImplemented
45+
46+
47+
def test_pauli_multiply_labels_unreachable_raises(monkeypatch):
48+
from shadowsim.core import pauli as pauli_module
49+
50+
monkeypatch.setattr(
51+
pauli_module,
52+
"_PAULI_MATRICES",
53+
{
54+
"I": np.zeros((2, 2), dtype=np.complex128),
55+
"X": np.array([[1, 2], [3, 4]], dtype=np.complex128),
56+
"Y": np.array([[5, 6], [7, 8]], dtype=np.complex128),
57+
"Z": np.array([[9, 0], [0, 1]], dtype=np.complex128),
58+
},
59+
)
60+
with pytest.raises(RuntimeError, match="unreachable"):
61+
pauli_module._multiply_labels("X", "Y")
62+

0 commit comments

Comments
 (0)