Skip to content

Commit 24cbed3

Browse files
committed
operator tests
1 parent 1cdc736 commit 24cbed3

4 files changed

Lines changed: 371 additions & 73 deletions

File tree

src/core/operator_set.py

Lines changed: 146 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,177 @@
11
import numpy as np
22

3-
from src.core.utils import hermitian, unitary
3+
from src.core.utils import (
4+
hermitian,
5+
indefinite,
6+
negative_semidefinite,
7+
positive_semidefinite,
8+
tensor,
9+
unitary,
10+
)
411

512

613
class Operator:
714
def __init__(self, matrix: np.ndarray, name: str = None):
15+
"""Initializes an Operator object which represents a quantum operator.
16+
17+
Parameter matrix: The matrix representation of the operator.
18+
Precondition: matrix is a numpy array.
19+
20+
Parameter name: The name of the operator.
21+
Precondition: name is a string or None.
22+
"""
23+
assert isinstance(matrix, np.ndarray), "matrix must be a numpy array"
24+
assert isinstance(name, str) or name is None, "name must be a string or None"
825
self.matrix = matrix
926
self.name = name
1027
self.dimension = matrix.shape[0]
1128
self.is_hermitian = hermitian(matrix)
1229
self.is_unitary = unitary(matrix)
13-
self.is_positive_semidefinite = np.all(np.linalg.eigvals(matrix) >= 0)
14-
self.is_negative_semidefinite = np.all(np.linalg.eigvals(matrix) <= 0)
15-
self.is_indefinite = np.any(np.linalg.eigvals(matrix) > 0) and np.any(
16-
np.linalg.eigvals(matrix) < 0
17-
)
30+
self.is_positive_semidefinite = positive_semidefinite(matrix)
31+
self.is_negative_semidefinite = negative_semidefinite(matrix)
32+
self.is_indefinite = indefinite(matrix)
1833

19-
def is_hermitian(self):
20-
return hermitian(self.matrix)
34+
def is_hermitian(self):
35+
"""Returns whether the operator is Hermitian."""
36+
return hermitian(self.matrix)
2137

22-
def is_unitary(self):
23-
return unitary(self.matrix)
38+
def is_unitary(self):
39+
"""Returns whether the operator is unitary."""
40+
return unitary(self.matrix)
2441

25-
def is_positive_semidefinite(self):
26-
return np.all(np.linalg.eigvals(self.matrix) >= 0)
42+
def is_positive_semidefinite(self):
43+
"""Returns whether the operator is positive semidefinite."""
44+
return positive_semidefinite(self.matrix)
2745

28-
def is_negative_semidefinite(self):
29-
return np.all(np.linalg.eigvals(self.matrix) <= 0)
46+
def is_negative_semidefinite(self):
47+
"""Returns whether the operator is negative semidefinite."""
48+
return negative_semidefinite(self.matrix)
3049

31-
def is_indefinite(self):
32-
return np.any(np.linalg.eigvals(self.matrix) > 0) and np.any(
33-
np.linalg.eigvals(self.matrix) < 0
34-
)
50+
def is_indefinite(self):
51+
"""Returns whether the operator is indefinite."""
52+
return indefinite(self.matrix)
3553

3654
def __str__(self):
55+
"""Returns a string representation of the operator."""
3756
return f"Operator(matrix={self.matrix})"
3857

3958
def __repr__(self):
59+
"""Returns a string representation of the operator."""
4060
return f"Operator(matrix={self.matrix})"
4161

4262
def __eq__(self, other):
63+
"""Returns whether the operator is equal to another operator."""
4364
return np.allclose(self.matrix, other.matrix)
4465

4566
def __ne__(self, other):
67+
"""Returns whether the operator is not equal to another operator."""
4668
return not np.allclose(self.matrix, other.matrix)
4769

4870
def to_operator_set(self):
71+
"""Returns an OperatorSet containing the operator."""
4972
return OperatorSet([self])
5073

51-
def to_local_operator(self, local_dim: int = 2):
52-
local_dim = int(local_dim)
53-
dim = self.dimension
54-
n_sites = 0
55-
p = 1
56-
while p < dim:
57-
p *= local_dim
58-
n_sites += 1
59-
if p != dim:
60-
raise ValueError(
61-
f"operator dimension {dim} is not {local_dim}**n for a non-negative "
62-
"integer n; cannot wrap as LocalOperator on a contiguous block"
63-
)
64-
return LocalOperator(self.matrix, list(range(n_sites)), local_dim)
74+
def to_local_operator(self, sites: list[int], local_dim: int = 2):
75+
"""Returns a LocalOperator representing the operator acting on the given sites."""
76+
return LocalOperator(self.matrix, sites, local_dim)
77+
78+
def set_name(self, name: str | None):
79+
"""Sets the operator name."""
80+
assert isinstance(name, str) or name is None, "name must be a string or None"
81+
self.name = name
6582

6683

6784
class LocalOperator(Operator):
6885
"""
69-
Operator acting non-trivially on the given `sites` of a tensor-product space.
70-
71-
``sites`` must be distinct integers forming a single contiguous block (e.g.
72-
``[1, 2, 3]`` or ``[3, 2, 1]``); gaps are not supported yet.
73-
74-
The `matrix` dimension must match ``local_dim ** len(sites)``.
86+
Represents an operator that acts non-trivially on the given `sites` of a
87+
tensor-product space.
7588
"""
7689

77-
def __init__(
78-
self,
79-
matrix: np.ndarray,
80-
sites: list[int],
81-
local_dim: int = 2,
82-
):
83-
sites_list = list(sites)
84-
if not all(isinstance(site, int) for site in sites_list):
85-
raise TypeError("all `sites` entries must be integers")
86-
if len(set(sites_list)) != len(sites_list):
87-
raise ValueError("all `sites` entries must be unique")
88-
if len(sites_list) > 1:
89-
lo, hi = min(sites_list), max(sites_list)
90-
if hi - lo + 1 != len(sites_list):
91-
raise ValueError(
92-
"non-consecutive site indices are not supported yet; "
93-
"`sites` must be a contiguous range of integers (e.g. "
94-
"`[0, 1, 2]`). "
95-
f"Got {sites_list!r}."
96-
)
97-
98-
matrix = np.asarray(matrix)
99-
if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
100-
raise ValueError("`matrix` must be a square 2D array")
101-
102-
local_dim = int(local_dim)
103-
if local_dim <= 0:
104-
raise ValueError("`local_dim` must be a positive integer")
105-
106-
expected_dim = local_dim ** len(sites_list)
90+
def __init__(self, matrix: np.ndarray, sites: list[int], local_dim: int = 2):
91+
"""Initializes a LocalOperator object which represents an operator that
92+
acts non-trivially on the given sites.
93+
94+
Parameter matrix: The matrix representation of the operator.
95+
Precondition: matrix is a numpy array.
96+
97+
Parameter sites: The sites on which the operator acts non-trivially.
98+
Precondition: sites is a list of contiguous integers.
99+
100+
Parameter local_dim: The local dimension of the operator.
101+
Precondition: local_dim is a positive integer."""
102+
assert isinstance(matrix, np.ndarray), "matrix must be a numpy array"
103+
assert (
104+
matrix.ndim == 2 and matrix.shape[0] == matrix.shape[1]
105+
), "matrix must be a square 2D array"
106+
assert isinstance(sites, list), "sites must be a list"
107+
assert all(
108+
isinstance(site, int) for site in sites
109+
), "all sites must be integers"
110+
assert len(set(sites)) == len(sites), "all sites must be unique"
111+
assert len(sites) >= 1, "there must be at least one site"
112+
assert max(sites) - min(sites) + 1 == len(
113+
sites
114+
), """sites
115+
must be a contiguous range of integers"""
116+
assert isinstance(local_dim, int), "local_dim must be an integer"
117+
assert local_dim > 0, "local_dim must be a positive integer"
118+
119+
expected_dim = local_dim ** len(sites)
107120
if matrix.shape[0] != expected_dim:
108121
raise ValueError(
109122
"invalid number of sites for the given matrix: "
110-
f"got len(sites)={len(sites_list)} and local_dim={local_dim}, "
123+
f"got len(sites)={len(sites)} and local_dim={local_dim}, "
111124
f"expected matrix dimension {expected_dim}x{expected_dim}, "
112125
f"but got {matrix.shape[0]}x{matrix.shape[1]}"
113126
)
114127

115128
super().__init__(matrix)
116-
self.sites = sites_list
129+
self.sites = sites
117130
self.local_dim = local_dim
118131

132+
def get_sites(self):
133+
"""Returns the sites on which the local operator acts non-trivially."""
134+
return self.sites
135+
136+
def get_local_dim(self):
137+
"""Returns the local dimension of the local operator."""
138+
return self.local_dim
139+
140+
def get_matrix(self):
141+
"""Returns the matrix representation of the local operator."""
142+
return self.matrix
143+
144+
def to_operator(self):
145+
"""Returns an Operator representing the local operator."""
146+
return Operator(self.matrix)
147+
148+
def to_full_operator(self, total_sites: int):
149+
"""Embed this local operator into a full system of ``total_sites``.
150+
151+
The operator acts on its contiguous block ``self.sites`` and identity
152+
acts on all other sites.
153+
154+
Parameter total_sites: The total number of sites in the full system.
155+
Precondition: total_sites is a positive integer.
156+
"""
157+
assert isinstance(total_sites, int), "total_sites must be an integer"
158+
assert total_sites >= 1, "total_sites must be positive"
159+
160+
lo, hi = min(self.sites), max(self.sites)
161+
assert lo >= 0, "site indices must be non-negative"
162+
if hi >= total_sites:
163+
raise ValueError(
164+
f"total_sites={total_sites} is too small for local sites {self.sites}"
165+
)
166+
167+
eye = np.eye(self.local_dim, dtype=self.matrix.dtype)
168+
left_id = [eye] * lo
169+
right_id = [eye] * (total_sites - hi - 1)
170+
full_matrix = tensor(left_id + [self.matrix] + right_id)
171+
return Operator(full_matrix)
172+
119173
def __str__(self):
174+
"""Returns a string representation of the local operator."""
120175
return (
121176
"LocalOperator("
122177
f"sites={self.sites}, "
@@ -126,6 +181,7 @@ def __str__(self):
126181
)
127182

128183
def __repr__(self):
184+
"""Returns a string representation of the local operator."""
129185
return (
130186
"LocalOperator("
131187
f"matrix={self.matrix!r}, "
@@ -137,20 +193,38 @@ def __repr__(self):
137193

138194
class OperatorSet:
139195
def __init__(self, operators: list[Operator]):
196+
"""Initializes an OperatorSet object which represents a set of quantum operators.
197+
198+
Parameter operators: The list of operators to initialize the operator set with.
199+
Precondition: operators is a list of Operator objects.
200+
"""
201+
assert isinstance(operators, list), "operators must be a list"
202+
assert all(
203+
isinstance(op, Operator) for op in operators
204+
), "all operators must be Operator objects"
140205
self.operators = operators
141206
self.operator_count = len(operators)
142207

143208
def __iter__(self):
209+
"""Returns an iterator over the operators in the operator set."""
144210
return iter(self.operators)
145211

146212
def __len__(self):
213+
"""Returns the number of operators in the operator set."""
147214
return len(self.operators)
148215

149-
def __getitem__(self, item):
150-
return self.operators[item]
216+
def __getitem__(self, index: int):
217+
"""Returns the operator at the given index."""
218+
assert isinstance(index, int), "index must be an integer"
219+
assert (
220+
index >= 0 and index < self.operator_count
221+
), "index must be within the range of the operator set"
222+
return self.operators[index]
151223

152224
def __str__(self):
225+
"""Returns a string representation of the operator set."""
153226
return f"OperatorSet(operator_count={self.operator_count})"
154227

155228
def __repr__(self):
229+
"""Returns a string representation of the operator set."""
156230
return f"OperatorSet(operators={self.operators!r})"

src/core/utils.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,37 @@ def hermitian(H):
3030
return np.allclose(H, H.conjugate().T)
3131

3232

33+
def _real_parts_of_eigenvalues(matrix):
34+
eigvals = np.linalg.eigvals(matrix)
35+
return np.real_if_close(eigvals, tol=1000).real
36+
37+
38+
def positive_semidefinite(matrix):
39+
"""Returns whether a matrix is positive semidefinite."""
40+
return np.all(_real_parts_of_eigenvalues(matrix) >= 0)
41+
42+
43+
def positive_definite(matrix):
44+
"""Returns whether a matrix is positive definite."""
45+
return np.all(_real_parts_of_eigenvalues(matrix) > 0)
46+
47+
48+
def negative_semidefinite(matrix):
49+
"""Returns whether a matrix is negative semidefinite."""
50+
return np.all(_real_parts_of_eigenvalues(matrix) <= 0)
51+
52+
53+
def negative_definite(matrix):
54+
"""Returns whether a matrix is negative definite."""
55+
return np.all(_real_parts_of_eigenvalues(matrix) < 0)
56+
57+
58+
def indefinite(matrix):
59+
"""Returns whether a matrix has both positive and negative eigenvalues."""
60+
eigvals = _real_parts_of_eigenvalues(matrix)
61+
return np.any(eigvals > 0) and np.any(eigvals < 0)
62+
63+
3364
def flip_dict(d):
3465
"""
3566
Given a dictionary d with a string key, it returns a new dictionary with

0 commit comments

Comments
 (0)