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.
13from __future__ import annotations
15import functools
16import itertools
17import math
18from typing import Dict, List, Tuple, Union
20import numpy as np
21from braket.circuits.gate import Gate
22from braket.circuits.observable import Observable, StandardObservable
23from braket.circuits.quantum_operator_helpers import (
24 get_pauli_eigenvalues,
25 is_hermitian,
26 verify_quantum_operator_matrix_dimensions,
27)
30class H(StandardObservable):
31 """Hadamard operation as an observable."""
33 def __init__(self):
34 """
35 Examples:
36 >>> Observable.I()
37 """
38 super().__init__(qubit_count=1, ascii_symbols=["H"])
40 def to_ir(self) -> List[str]:
41 return ["h"]
43 def to_matrix(self) -> np.ndarray:
44 return 1.0 / np.sqrt(2.0) * np.array([[1.0, 1.0], [1.0, -1.0]], dtype=complex)
46 @property
47 def basis_rotation_gates(self) -> Tuple[Gate]:
48 return tuple([Gate.Ry(-math.pi / 4)])
51Observable.register_observable(H)
54class I(Observable): # noqa: E742, E261
55 """Identity operation as an observable."""
57 def __init__(self):
58 """
59 Examples:
60 >>> Observable.I()
61 """
62 super().__init__(qubit_count=1, ascii_symbols=["I"])
64 def to_ir(self) -> List[str]:
65 return ["i"]
67 def to_matrix(self) -> np.ndarray:
68 return np.array([[1.0, 0.0], [0.0, 1.0]], dtype=complex)
70 @property
71 def basis_rotation_gates(self) -> Tuple[Gate]:
72 return ()
74 @property
75 def eigenvalues(self) -> np.ndarray:
76 return np.array([1, 1])
79Observable.register_observable(I)
82class X(StandardObservable):
83 """Pauli-X operation as an observable."""
85 def __init__(self):
86 """
87 Examples:
88 >>> Observable.X()
89 """
90 super().__init__(qubit_count=1, ascii_symbols=["X"])
92 def to_ir(self) -> List[str]:
93 return ["x"]
95 def to_matrix(self) -> np.ndarray:
96 return np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex)
98 @property
99 def basis_rotation_gates(self) -> Tuple[Gate]:
100 return tuple([Gate.H()])
103Observable.register_observable(X)
106class Y(StandardObservable):
107 """Pauli-Y operation as an observable."""
109 def __init__(self):
110 """
111 Examples:
112 >>> Observable.Y()
113 """
114 super().__init__(qubit_count=1, ascii_symbols=["Y"])
116 def to_ir(self) -> List[str]:
117 return ["y"]
119 def to_matrix(self) -> np.ndarray:
120 return np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex)
122 @property
123 def basis_rotation_gates(self) -> Tuple[Gate]:
124 return tuple([Gate.Z(), Gate.S(), Gate.H()])
127Observable.register_observable(Y)
130class Z(StandardObservable):
131 """Pauli-Z operation as an observable."""
133 def __init__(self):
134 """
135 Examples:
136 >>> Observable.Z()
137 """
138 super().__init__(qubit_count=1, ascii_symbols=["Z"])
140 def to_ir(self) -> List[str]:
141 return ["z"]
143 def to_matrix(self) -> np.ndarray:
144 return np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex)
146 @property
147 def basis_rotation_gates(self) -> Tuple[Gate]:
148 return ()
151Observable.register_observable(Z)
154class TensorProduct(Observable):
155 """Tensor product of observables"""
157 def __init__(self, observables: List[Observable]):
158 """
159 Args:
160 observables (List[Observable]): List of observables for tensor product
162 Examples:
163 >>> t1 = Observable.Y() @ Observable.X()
164 >>> t1.to_matrix()
165 array([[0.+0.j, 0.+0.j, 0.-0.j, 0.-1.j],
166 [0.+0.j, 0.+0.j, 0.-1.j, 0.-0.j],
167 [0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j],
168 [0.+1.j, 0.+0.j, 0.+0.j, 0.+0.j]])
169 >>> t2 = Observable.Z() @ t1
170 >>> t2.observables
171 (Z('qubit_count': 1), Y('qubit_count': 1), X('qubit_count': 1))
173 Note: list of observables for tensor product must be given in the desired order that
174 the tensor product will be calculated. For `TensorProduct(observables=[ob1, ob2, ob3])`,
175 the tensor product's matrix will be the result of the tensor product of `ob1`, `ob2`,
176 `ob3`, or `np.kron(np.kron(ob1.to_matrix(), ob2.to_matrix()), ob3.to_matrix())`
177 """
178 self._observables = tuple(observables)
179 qubit_count = sum([obs.qubit_count for obs in observables])
180 display_name = "@".join([obs.ascii_symbols[0] for obs in observables])
181 super().__init__(qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count)
182 self._eigenvalues = TensorProduct._compute_eigenvalues(self._observables, qubit_count)
184 def to_ir(self) -> List[str]:
185 ir = []
186 for obs in self.observables:
187 ir.extend(obs.to_ir())
188 return ir
190 @property
191 def observables(self) -> Tuple[Observable]:
192 """Tuple[Observable]: observables part of tensor product"""
193 return self._observables
195 def to_matrix(self) -> np.ndarray:
196 return functools.reduce(np.kron, [obs.to_matrix() for obs in self.observables])
198 @property
199 def basis_rotation_gates(self) -> Tuple[Gate]:
200 gates = []
201 for obs in self.observables:
202 gates.extend(obs.basis_rotation_gates)
203 return tuple(gates)
205 @property
206 def eigenvalues(self):
207 return self._eigenvalues
209 def __matmul__(self, other):
210 if isinstance(other, TensorProduct):
211 return TensorProduct(list(self.observables) + list(other.observables))
213 if isinstance(other, Observable):
214 return TensorProduct(list(self.observables) + [other])
216 raise ValueError("Can only perform tensor products between observables.")
218 def __rmatmul__(self, other):
219 if isinstance(other, Observable):
220 return TensorProduct([other] + list(self.observables))
222 raise ValueError("Can only perform tensor products between observables.")
224 def __repr__(self):
225 return "TensorProduct(" + ", ".join([repr(o) for o in self.observables]) + ")"
227 def __eq__(self, other):
228 return self.matrix_equivalence(other)
230 @staticmethod
231 def _compute_eigenvalues(observables: Tuple[Observable], num_qubits: int) -> np.ndarray:
232 if False in [isinstance(observable, StandardObservable) for observable in observables]:
233 # Tensor product of observables contains a mixture
234 # of standard and non-standard observables
235 eigenvalues = np.array([1])
236 for k, g in itertools.groupby(observables, lambda x: isinstance(x, StandardObservable)):
237 if k:
238 # Subgroup g contains only standard observables.
239 eigenvalues = np.kron(eigenvalues, get_pauli_eigenvalues(len(list(g))))
240 else:
241 # Subgroup g contains only non-standard observables.
242 for nonstandard in g:
243 # loop through all non-standard observables
244 eigenvalues = np.kron(eigenvalues, nonstandard.eigenvalues)
245 else:
246 eigenvalues = get_pauli_eigenvalues(num_qubits=num_qubits)
248 eigenvalues.setflags(write=False)
249 return eigenvalues
252Observable.register_observable(TensorProduct)
255class Hermitian(Observable):
256 """Hermitian matrix as an observable."""
258 # Cache of eigenpairs
259 _eigenpairs = {}
261 def __init__(self, matrix: np.ndarray, display_name: str = "Hermitian"):
262 """
263 Args:
264 matrix (numpy.ndarray): Hermitian matrix which defines the observable.
265 display_name (str): Name to be used for an instance of this Hermitian matrix
266 observable for circuit diagrams. Defaults to `Hermitian`.
268 Raises:
269 ValueError: If `matrix` is not a two-dimensional square matrix,
270 or has a dimension length which is not a positive exponent of 2,
271 or is non-hermitian.
273 Example:
274 >>> Observable.Hermitian(matrix=np.array([[0, 1],[1, 0]]))
275 """
276 verify_quantum_operator_matrix_dimensions(matrix)
277 self._matrix = np.array(matrix, dtype=complex)
278 qubit_count = int(np.log2(self._matrix.shape[0]))
280 if not is_hermitian(self._matrix):
281 raise ValueError(f"{self._matrix} is not hermitian")
283 super().__init__(qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count)
285 def to_ir(self) -> List[List[List[List[float]]]]:
286 return [
287 [[[element.real, element.imag] for element in row] for row in self._matrix.tolist()]
288 ]
290 def to_matrix(self) -> np.ndarray:
291 return self._matrix
293 def __eq__(self, other) -> bool:
294 return self.matrix_equivalence(other)
296 @property
297 def basis_rotation_gates(self) -> Tuple[Gate]:
298 return tuple([Gate.Unitary(matrix=self._get_eigendecomposition()["eigenvectors_conj_t"])])
300 @property
301 def eigenvalues(self):
302 return self._get_eigendecomposition()["eigenvalues"]
304 def _get_eigendecomposition(self) -> Dict[str, np.ndarray]:
305 """
306 Decomposes the Hermitian matrix into its eigenvectors and associated eigenvalues.
307 The eigendecomposition is cached so that if another Hermitian observable
308 is created with the same matrix, the eigendecomposition doesn't have to
309 be recalculated.
311 Returns:
312 Dict[str, np.ndarray]: The keys are "eigenvectors_conj_t", mapping to the
313 conjugate transpose of a matrix whose columns are the eigenvectors of the matrix,
314 and "eigenvalues", a list of associated eigenvalues in the order of their
315 corresponding eigenvectors in the "eigenvectors" matrix. These cached values
316 are immutable.
317 """
318 mat_key = tuple(self._matrix.flatten().tolist())
319 if mat_key not in Hermitian._eigenpairs:
320 eigenvalues, eigenvectors = np.linalg.eigh(self._matrix)
321 eigenvalues.setflags(write=False)
322 eigenvectors_conj_t = eigenvectors.conj().T
323 eigenvectors_conj_t.setflags(write=False)
324 Hermitian._eigenpairs[mat_key] = {
325 "eigenvectors_conj_t": eigenvectors_conj_t,
326 "eigenvalues": eigenvalues,
327 }
328 return Hermitian._eigenpairs[mat_key]
331Observable.register_observable(Hermitian)
334def observable_from_ir(ir_observable: List[Union[str, List[List[List[float]]]]]) -> Observable:
335 """
336 Create an observable from the IR observable list. This can be a tensor product of
337 observables or a single observable.
339 Args:
340 ir_observable (List[Union[str, List[List[List[float]]]]]): observable as defined in IR
342 Return:
343 Observable: observable object
344 """
345 if len(ir_observable) == 1:
346 return _observable_from_ir_list_item(ir_observable[0])
347 else:
348 observable = TensorProduct([_observable_from_ir_list_item(obs) for obs in ir_observable])
349 return observable
352def _observable_from_ir_list_item(observable: Union[str, List[List[List[float]]]]) -> Observable:
353 if observable == "i":
354 return I()
355 elif observable == "h":
356 return H()
357 elif observable == "x":
358 return X()
359 elif observable == "y":
360 return Y()
361 elif observable == "z":
362 return Z()
363 else:
364 try:
365 matrix = np.array(
366 [[complex(element[0], element[1]) for element in row] for row in observable]
367 )
368 return Hermitian(matrix)
369 except Exception as e:
370 raise ValueError(f"Invalid observable specified: {observable} error: {e}")