Coverage for src/braket/circuits/observables.py : 100%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright 2019-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"). You
4# may not use this file except in compliance with the License. A copy of
5# the License is located at
6#
7# http://aws.amazon.com/apache2.0/
8#
9# or in the "license" file accompanying this file. This file is
10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11# ANY KIND, either express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
14from __future__ import annotations
16import functools
17import itertools
18import math
19from typing import Dict, List, Tuple, Union
21import numpy as np
22from braket.circuits.gate import Gate
23from braket.circuits.observable import Observable, StandardObservable
24from braket.circuits.quantum_operator_helpers import (
25 get_pauli_eigenvalues,
26 is_hermitian,
27 verify_quantum_operator_matrix_dimensions,
28)
31class H(StandardObservable):
32 """Hadamard operation as an observable."""
34 def __init__(self):
35 """
36 Examples:
37 >>> Observable.I()
38 """
39 super().__init__(qubit_count=1, ascii_symbols=["H"])
41 def to_ir(self) -> List[str]:
42 return ["h"]
44 def to_matrix(self) -> np.ndarray:
45 return 1.0 / np.sqrt(2.0) * np.array([[1.0, 1.0], [1.0, -1.0]], dtype=complex)
47 @property
48 def basis_rotation_gates(self) -> Tuple[Gate]:
49 return tuple([Gate.Ry(-math.pi / 4)])
52Observable.register_observable(H)
55class I(Observable): # noqa: E742, E261
56 """Identity operation as an observable."""
58 def __init__(self):
59 """
60 Examples:
61 >>> Observable.I()
62 """
63 super().__init__(qubit_count=1, ascii_symbols=["I"])
65 def to_ir(self) -> List[str]:
66 return ["i"]
68 def to_matrix(self) -> np.ndarray:
69 return np.array([[1.0, 0.0], [0.0, 1.0]], dtype=complex)
71 @property
72 def basis_rotation_gates(self) -> Tuple[Gate]:
73 return ()
75 @property
76 def eigenvalues(self) -> np.ndarray:
77 return np.array([1, 1])
80Observable.register_observable(I)
83class X(StandardObservable):
84 """Pauli-X operation as an observable."""
86 def __init__(self):
87 """
88 Examples:
89 >>> Observable.X()
90 """
91 super().__init__(qubit_count=1, ascii_symbols=["X"])
93 def to_ir(self) -> List[str]:
94 return ["x"]
96 def to_matrix(self) -> np.ndarray:
97 return np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex)
99 @property
100 def basis_rotation_gates(self) -> Tuple[Gate]:
101 return tuple([Gate.H()])
104Observable.register_observable(X)
107class Y(StandardObservable):
108 """Pauli-Y operation as an observable."""
110 def __init__(self):
111 """
112 Examples:
113 >>> Observable.Y()
114 """
115 super().__init__(qubit_count=1, ascii_symbols=["Y"])
117 def to_ir(self) -> List[str]:
118 return ["y"]
120 def to_matrix(self) -> np.ndarray:
121 return np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex)
123 @property
124 def basis_rotation_gates(self) -> Tuple[Gate]:
125 return tuple([Gate.Z(), Gate.S(), Gate.H()])
128Observable.register_observable(Y)
131class Z(StandardObservable):
132 """Pauli-Z operation as an observable."""
134 def __init__(self):
135 """
136 Examples:
137 >>> Observable.Z()
138 """
139 super().__init__(qubit_count=1, ascii_symbols=["Z"])
141 def to_ir(self) -> List[str]:
142 return ["z"]
144 def to_matrix(self) -> np.ndarray:
145 return np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex)
147 @property
148 def basis_rotation_gates(self) -> Tuple[Gate]:
149 return ()
152Observable.register_observable(Z)
155class TensorProduct(Observable):
156 """Tensor product of observables"""
158 def __init__(self, observables: List[Observable]):
159 """
160 Args:
161 observables (List[Observable]): List of observables for tensor product
163 Examples:
164 >>> t1 = Observable.Y() @ Observable.X()
165 >>> t1.to_matrix()
166 array([[0.+0.j, 0.+0.j, 0.-0.j, 0.-1.j],
167 [0.+0.j, 0.+0.j, 0.-1.j, 0.-0.j],
168 [0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j],
169 [0.+1.j, 0.+0.j, 0.+0.j, 0.+0.j]])
170 >>> t2 = Observable.Z() @ t1
171 >>> t2.factors
172 (Z('qubit_count': 1), Y('qubit_count': 1), X('qubit_count': 1))
174 Note: You must provide the list of observables for the tensor product to be evaluated
175 in the order that you want the tensor product to be calculated.
176 For `TensorProduct(observables=[ob1, ob2, ob3])`, the tensor product's matrix is the
177 result of the tensor product of `ob1`, `ob2`, `ob3`, or `np.kron(np.kron(ob1.to_matrix(),
178 ob2.to_matrix()), ob3.to_matrix())`.
179 """
180 self._observables = tuple(observables)
181 qubit_count = sum([obs.qubit_count for obs in observables])
182 display_name = "@".join([obs.ascii_symbols[0] for obs in observables])
183 super().__init__(qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count)
184 self._eigenvalues = TensorProduct._compute_eigenvalues(self._observables, qubit_count)
186 def to_ir(self) -> List[str]:
187 ir = []
188 for obs in self.factors:
189 ir.extend(obs.to_ir())
190 return ir
192 @property
193 def factors(self) -> Tuple[Observable]:
194 """ Tuple[Observable]: The observables that comprise this tensor product."""
195 return self._observables
197 def to_matrix(self) -> np.ndarray:
198 return functools.reduce(np.kron, [obs.to_matrix() for obs in self.factors])
200 @property
201 def basis_rotation_gates(self) -> Tuple[Gate]:
202 gates = []
203 for obs in self.factors:
204 gates.extend(obs.basis_rotation_gates)
205 return tuple(gates)
207 @property
208 def eigenvalues(self):
209 return self._eigenvalues
211 def __matmul__(self, other):
212 if isinstance(other, TensorProduct):
213 return TensorProduct(list(self.factors) + list(other.factors))
215 if isinstance(other, Observable):
216 return TensorProduct(list(self.factors) + [other])
218 raise ValueError("Can only perform tensor products between observables.")
220 def __rmatmul__(self, other):
221 if isinstance(other, Observable):
222 return TensorProduct([other] + list(self.factors))
224 raise ValueError("Can only perform tensor products between observables.")
226 def __repr__(self):
227 return "TensorProduct(" + ", ".join([repr(o) for o in self.factors]) + ")"
229 def __eq__(self, other):
230 return self.matrix_equivalence(other)
232 @staticmethod
233 def _compute_eigenvalues(observables: Tuple[Observable], num_qubits: int) -> np.ndarray:
234 if False in [isinstance(observable, StandardObservable) for observable in observables]:
235 # Tensor product of observables contains a mixture
236 # of standard and non-standard observables
237 eigenvalues = np.array([1])
238 for k, g in itertools.groupby(observables, lambda x: isinstance(x, StandardObservable)):
239 if k:
240 # Subgroup g contains only standard observables.
241 eigenvalues = np.kron(eigenvalues, get_pauli_eigenvalues(len(list(g))))
242 else:
243 # Subgroup g contains only non-standard observables.
244 for nonstandard in g:
245 # loop through all non-standard observables
246 eigenvalues = np.kron(eigenvalues, nonstandard.eigenvalues)
247 else:
248 eigenvalues = get_pauli_eigenvalues(num_qubits=num_qubits)
250 eigenvalues.setflags(write=False)
251 return eigenvalues
254Observable.register_observable(TensorProduct)
257class Hermitian(Observable):
258 """Hermitian matrix as an observable."""
260 # Cache of eigenpairs
261 _eigenpairs = {}
263 def __init__(self, matrix: np.ndarray, display_name: str = "Hermitian"):
264 """
265 Args:
266 matrix (numpy.ndarray): Hermitian matrix that defines the observable.
267 display_name (str): Name to use for an instance of this Hermitian matrix
268 observable for circuit diagrams. Defaults to `Hermitian`.
270 Raises:
271 ValueError: If `matrix` is not a two-dimensional square matrix,
272 or has a dimension length that is not a positive exponent of 2,
273 or is non-hermitian.
275 Example:
276 >>> Observable.Hermitian(matrix=np.array([[0, 1],[1, 0]]))
277 """
278 verify_quantum_operator_matrix_dimensions(matrix)
279 self._matrix = np.array(matrix, dtype=complex)
280 qubit_count = int(np.log2(self._matrix.shape[0]))
282 if not is_hermitian(self._matrix):
283 raise ValueError(f"{self._matrix} is not hermitian")
285 super().__init__(qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count)
287 def to_ir(self) -> List[List[List[List[float]]]]:
288 return [
289 [[[element.real, element.imag] for element in row] for row in self._matrix.tolist()]
290 ]
292 def to_matrix(self) -> np.ndarray:
293 return self._matrix
295 def __eq__(self, other) -> bool:
296 return self.matrix_equivalence(other)
298 @property
299 def basis_rotation_gates(self) -> Tuple[Gate]:
300 return tuple([Gate.Unitary(matrix=self._get_eigendecomposition()["eigenvectors_conj_t"])])
302 @property
303 def eigenvalues(self):
304 return self._get_eigendecomposition()["eigenvalues"]
306 def _get_eigendecomposition(self) -> Dict[str, np.ndarray]:
307 """
308 Decomposes the Hermitian matrix into its eigenvectors and associated eigenvalues.
309 The eigendecomposition is cached so that if another Hermitian observable
310 is created with the same matrix, the eigendecomposition doesn't have to
311 be recalculated.
313 Returns:
314 Dict[str, np.ndarray]: The keys are "eigenvectors_conj_t", mapping to the
315 conjugate transpose of a matrix whose columns are the eigenvectors of the matrix,
316 and "eigenvalues", a list of associated eigenvalues in the order of their
317 corresponding eigenvectors in the "eigenvectors" matrix. These cached values
318 are immutable.
319 """
320 mat_key = tuple(self._matrix.flatten().tolist())
321 if mat_key not in Hermitian._eigenpairs:
322 eigenvalues, eigenvectors = np.linalg.eigh(self._matrix)
323 eigenvalues.setflags(write=False)
324 eigenvectors_conj_t = eigenvectors.conj().T
325 eigenvectors_conj_t.setflags(write=False)
326 Hermitian._eigenpairs[mat_key] = {
327 "eigenvectors_conj_t": eigenvectors_conj_t,
328 "eigenvalues": eigenvalues,
329 }
330 return Hermitian._eigenpairs[mat_key]
333Observable.register_observable(Hermitian)
336def observable_from_ir(ir_observable: List[Union[str, List[List[List[float]]]]]) -> Observable:
337 """
338 Create an observable from the IR observable list. This can be a tensor product of
339 observables or a single observable.
341 Args:
342 ir_observable (List[Union[str, List[List[List[float]]]]]): observable as defined in IR
344 Return:
345 Observable: observable object
346 """
347 if len(ir_observable) == 1:
348 return _observable_from_ir_list_item(ir_observable[0])
349 else:
350 observable = TensorProduct([_observable_from_ir_list_item(obs) for obs in ir_observable])
351 return observable
354def _observable_from_ir_list_item(observable: Union[str, List[List[List[float]]]]) -> Observable:
355 if observable == "i":
356 return I()
357 elif observable == "h":
358 return H()
359 elif observable == "x":
360 return X()
361 elif observable == "y":
362 return Y()
363 elif observable == "z":
364 return Z()
365 else:
366 try:
367 matrix = np.array(
368 [[complex(element[0], element[1]) for element in row] for row in observable]
369 )
370 return Hermitian(matrix)
371 except Exception as e:
372 raise ValueError(f"Invalid observable specified: {observable} error: {e}")