# Copyright 2019-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from __future__ import annotations
import functools
import itertools
import math
from typing import Dict, List, Tuple, Union
import numpy as np
from braket.circuits.gate import Gate
from braket.circuits.observable import Observable, StandardObservable
from braket.circuits.quantum_operator_helpers import (
get_pauli_eigenvalues,
is_hermitian,
verify_quantum_operator_matrix_dimensions,
)
[docs]class H(StandardObservable):
"""Hadamard operation as an observable."""
def __init__(self):
"""
Examples:
>>> Observable.I()
"""
super().__init__(qubit_count=1, ascii_symbols=["H"])
[docs] def to_ir(self) -> List[str]:
return ["h"]
[docs] def to_matrix(self) -> np.ndarray:
return 1.0 / np.sqrt(2.0) * np.array([[1.0, 1.0], [1.0, -1.0]], dtype=complex)
@property
def basis_rotation_gates(self) -> Tuple[Gate]:
return tuple([Gate.Ry(-math.pi / 4)])
Observable.register_observable(H)
[docs]class I(Observable): # noqa: E742, E261
"""Identity operation as an observable."""
def __init__(self):
"""
Examples:
>>> Observable.I()
"""
super().__init__(qubit_count=1, ascii_symbols=["I"])
[docs] def to_ir(self) -> List[str]:
return ["i"]
[docs] def to_matrix(self) -> np.ndarray:
return np.array([[1.0, 0.0], [0.0, 1.0]], dtype=complex)
@property
def basis_rotation_gates(self) -> Tuple[Gate]:
return ()
@property
def eigenvalues(self) -> np.ndarray:
return np.array([1, 1])
Observable.register_observable(I)
[docs]class X(StandardObservable):
"""Pauli-X operation as an observable."""
def __init__(self):
"""
Examples:
>>> Observable.X()
"""
super().__init__(qubit_count=1, ascii_symbols=["X"])
[docs] def to_ir(self) -> List[str]:
return ["x"]
[docs] def to_matrix(self) -> np.ndarray:
return np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex)
@property
def basis_rotation_gates(self) -> Tuple[Gate]:
return tuple([Gate.H()])
Observable.register_observable(X)
[docs]class Y(StandardObservable):
"""Pauli-Y operation as an observable."""
def __init__(self):
"""
Examples:
>>> Observable.Y()
"""
super().__init__(qubit_count=1, ascii_symbols=["Y"])
[docs] def to_ir(self) -> List[str]:
return ["y"]
[docs] def to_matrix(self) -> np.ndarray:
return np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex)
@property
def basis_rotation_gates(self) -> Tuple[Gate]:
return tuple([Gate.Z(), Gate.S(), Gate.H()])
Observable.register_observable(Y)
[docs]class Z(StandardObservable):
"""Pauli-Z operation as an observable."""
def __init__(self):
"""
Examples:
>>> Observable.Z()
"""
super().__init__(qubit_count=1, ascii_symbols=["Z"])
[docs] def to_ir(self) -> List[str]:
return ["z"]
[docs] def to_matrix(self) -> np.ndarray:
return np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex)
@property
def basis_rotation_gates(self) -> Tuple[Gate]:
return ()
Observable.register_observable(Z)
[docs]class TensorProduct(Observable):
"""Tensor product of observables"""
def __init__(self, observables: List[Observable]):
"""
Args:
observables (List[Observable]): List of observables for tensor product
Examples:
>>> t1 = Observable.Y() @ Observable.X()
>>> t1.to_matrix()
array([[0.+0.j, 0.+0.j, 0.-0.j, 0.-1.j],
[0.+0.j, 0.+0.j, 0.-1.j, 0.-0.j],
[0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j],
[0.+1.j, 0.+0.j, 0.+0.j, 0.+0.j]])
>>> t2 = Observable.Z() @ t1
>>> t2.observables
(Z('qubit_count': 1), Y('qubit_count': 1), X('qubit_count': 1))
Note: list of observables for tensor product must be given in the desired order that
the tensor product will be calculated. For `TensorProduct(observables=[ob1, ob2, ob3])`,
the tensor product's matrix will be the result of the tensor product of `ob1`, `ob2`,
`ob3`, or `np.kron(np.kron(ob1.to_matrix(), ob2.to_matrix()), ob3.to_matrix())`
"""
self._observables = tuple(observables)
qubit_count = sum([obs.qubit_count for obs in observables])
display_name = "@".join([obs.ascii_symbols[0] for obs in observables])
super().__init__(qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count)
self._eigenvalues = TensorProduct._compute_eigenvalues(self._observables, qubit_count)
[docs] def to_ir(self) -> List[str]:
ir = []
for obs in self.observables:
ir.extend(obs.to_ir())
return ir
@property
def observables(self) -> Tuple[Observable]:
"""Tuple[Observable]: observables part of tensor product"""
return self._observables
[docs] def to_matrix(self) -> np.ndarray:
return functools.reduce(np.kron, [obs.to_matrix() for obs in self.observables])
@property
def basis_rotation_gates(self) -> Tuple[Gate]:
gates = []
for obs in self.observables:
gates.extend(obs.basis_rotation_gates)
return tuple(gates)
@property
def eigenvalues(self):
return self._eigenvalues
def __matmul__(self, other):
if isinstance(other, TensorProduct):
return TensorProduct(list(self.observables) + list(other.observables))
if isinstance(other, Observable):
return TensorProduct(list(self.observables) + [other])
raise ValueError("Can only perform tensor products between observables.")
def __rmatmul__(self, other):
if isinstance(other, Observable):
return TensorProduct([other] + list(self.observables))
raise ValueError("Can only perform tensor products between observables.")
def __repr__(self):
return "TensorProduct(" + ", ".join([repr(o) for o in self.observables]) + ")"
def __eq__(self, other):
return self.matrix_equivalence(other)
@staticmethod
def _compute_eigenvalues(observables: Tuple[Observable], num_qubits: int) -> np.ndarray:
if False in [isinstance(observable, StandardObservable) for observable in observables]:
# Tensor product of observables contains a mixture
# of standard and non-standard observables
eigenvalues = np.array([1])
for k, g in itertools.groupby(observables, lambda x: isinstance(x, StandardObservable)):
if k:
# Subgroup g contains only standard observables.
eigenvalues = np.kron(eigenvalues, get_pauli_eigenvalues(len(list(g))))
else:
# Subgroup g contains only non-standard observables.
for nonstandard in g:
# loop through all non-standard observables
eigenvalues = np.kron(eigenvalues, nonstandard.eigenvalues)
else:
eigenvalues = get_pauli_eigenvalues(num_qubits=num_qubits)
eigenvalues.setflags(write=False)
return eigenvalues
Observable.register_observable(TensorProduct)
[docs]class Hermitian(Observable):
"""Hermitian matrix as an observable."""
# Cache of eigenpairs
_eigenpairs = {}
def __init__(self, matrix: np.ndarray, display_name: str = "Hermitian"):
"""
Args:
matrix (numpy.ndarray): Hermitian matrix which defines the observable.
display_name (str): Name to be used for an instance of this Hermitian matrix
observable for circuit diagrams. Defaults to `Hermitian`.
Raises:
ValueError: If `matrix` is not a two-dimensional square matrix,
or has a dimension length which is not a positive exponent of 2,
or is non-hermitian.
Example:
>>> Observable.Hermitian(matrix=np.array([[0, 1],[1, 0]]))
"""
verify_quantum_operator_matrix_dimensions(matrix)
self._matrix = np.array(matrix, dtype=complex)
qubit_count = int(np.log2(self._matrix.shape[0]))
if not is_hermitian(self._matrix):
raise ValueError(f"{self._matrix} is not hermitian")
super().__init__(qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count)
[docs] def to_ir(self) -> List[List[List[List[float]]]]:
return [
[[[element.real, element.imag] for element in row] for row in self._matrix.tolist()]
]
[docs] def to_matrix(self) -> np.ndarray:
return self._matrix
def __eq__(self, other) -> bool:
return self.matrix_equivalence(other)
@property
def basis_rotation_gates(self) -> Tuple[Gate]:
return tuple([Gate.Unitary(matrix=self._get_eigendecomposition()["eigenvectors_conj_t"])])
@property
def eigenvalues(self):
return self._get_eigendecomposition()["eigenvalues"]
def _get_eigendecomposition(self) -> Dict[str, np.ndarray]:
"""
Decomposes the Hermitian matrix into its eigenvectors and associated eigenvalues.
The eigendecomposition is cached so that if another Hermitian observable
is created with the same matrix, the eigendecomposition doesn't have to
be recalculated.
Returns:
Dict[str, np.ndarray]: The keys are "eigenvectors_conj_t", mapping to the
conjugate transpose of a matrix whose columns are the eigenvectors of the matrix,
and "eigenvalues", a list of associated eigenvalues in the order of their
corresponding eigenvectors in the "eigenvectors" matrix. These cached values
are immutable.
"""
mat_key = tuple(self._matrix.flatten().tolist())
if mat_key not in Hermitian._eigenpairs:
eigenvalues, eigenvectors = np.linalg.eigh(self._matrix)
eigenvalues.setflags(write=False)
eigenvectors_conj_t = eigenvectors.conj().T
eigenvectors_conj_t.setflags(write=False)
Hermitian._eigenpairs[mat_key] = {
"eigenvectors_conj_t": eigenvectors_conj_t,
"eigenvalues": eigenvalues,
}
return Hermitian._eigenpairs[mat_key]
Observable.register_observable(Hermitian)
[docs]def observable_from_ir(ir_observable: List[Union[str, List[List[List[float]]]]]) -> Observable:
"""
Create an observable from the IR observable list. This can be a tensor product of
observables or a single observable.
Args:
ir_observable (List[Union[str, List[List[List[float]]]]]): observable as defined in IR
Return:
Observable: observable object
"""
if len(ir_observable) == 1:
return _observable_from_ir_list_item(ir_observable[0])
else:
observable = TensorProduct([_observable_from_ir_list_item(obs) for obs in ir_observable])
return observable
def _observable_from_ir_list_item(observable: Union[str, List[List[List[float]]]]) -> Observable:
if observable == "i":
return I()
elif observable == "h":
return H()
elif observable == "x":
return X()
elif observable == "y":
return Y()
elif observable == "z":
return Z()
else:
try:
matrix = np.array(
[[complex(element[0], element[1]) for element in row] for row in observable]
)
return Hermitian(matrix)
except Exception as e:
raise ValueError(f"Invalid observable specified: {observable} error: {e}")