# Copyright 2019-2024 Cambridge Quantum Computing
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License 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.
import copy
from typing import TYPE_CHECKING, Any, Union
import numpy
import numpy as np
from sympy import Expr, Symbol, im, re, sympify
from pytket.circuit import Qubit
from pytket.pauli import QubitPauliString, pauli_string_mult
from pytket.utils.serialization import complex_to_list, list_to_complex
CoeffTypeAccepted = Union[int, float, complex, Expr]
if TYPE_CHECKING:
from scipy.sparse import csc_matrix
def _coeff_convert(coeff: CoeffTypeAccepted | str) -> Expr:
sympy_val = sympify(coeff)
if not isinstance(sympy_val, Expr):
raise ValueError("Unsupported value for QubitPauliString coefficient")
return sympy_val
[docs]
class QubitPauliOperator:
"""
Generic data structure for generation of circuits and expectation
value calculation. Contains a dictionary from QubitPauliString to
sympy Expr. Capacity for symbolic expressions allows the operator
to be used to generate ansätze for variational algorithms.
Represents a mathematical object :math:`\\sum_j \\alpha_j P_j`,
where each :math:`\\alpha_j` is a complex symbolic expression and
:math:`P_j` is a Pauli string, i.e. :math:`P_j \\in \\{ I, X, Y,
Z\\}^{\\otimes n}`.
A prototypical example is a molecular Hamiltonian, for which one
may wish to calculate the expectation value :math:`\\langle \\Psi
| H | \\Psi \\rangle` by decomposing :math:`H` into individual
Pauli measurements. Alternatively, one may wish to evolve a state
by the operator :math:`e^{-iHt}` for digital quantum simulation.
In this case, the whole operator must be decomposed into native
operations.
In both cases, :math:`H` may be represented by a
QubitPauliOperator.
"""
[docs]
def __init__(
self,
dictionary: dict[QubitPauliString, CoeffTypeAccepted] | None = None,
) -> None:
self._dict: dict[QubitPauliString, Expr] = dict()
if dictionary:
for key, value in dictionary.items():
self._dict[key] = _coeff_convert(value)
self._collect_qubits()
def __repr__(self) -> str:
return self._dict.__repr__()
def __getitem__(self, key: QubitPauliString) -> Expr:
return self._dict[key]
def get(self, key: QubitPauliString, default: CoeffTypeAccepted) -> Expr:
return self._dict.get(key, _coeff_convert(default))
def __setitem__(self, key: QubitPauliString, value: CoeffTypeAccepted) -> None:
"""Update value in dictionary ([]). Automatically converts value into sympy
Expr.
:param key: String to use as key
:type key: QubitPauliString
:param value: Associated coefficient
:type value: Union[int, float, complex, Expr]
"""
self._dict[key] = _coeff_convert(value)
self._all_qubits.update(key.map.keys())
def __getstate__(self) -> dict[QubitPauliString, Expr]:
return self._dict
def __setstate__(self, _dict: dict[QubitPauliString, Expr]) -> None:
# values assumed to be already sympified
self._dict = _dict
self._collect_qubits()
def __eq__(self, other: object) -> bool:
if isinstance(other, QubitPauliOperator):
return self._dict == other._dict
return False
def __iadd__(self, addend: "QubitPauliOperator") -> "QubitPauliOperator":
"""In-place addition (+=) of QubitPauliOperators.
:param addend: The operator to add
:type addend: QubitPauliOperator
:return: Updated operator (self)
:rtype: QubitPauliOperator
"""
if isinstance(addend, QubitPauliOperator):
for key, value in addend._dict.items():
self[key] = self.get(key, 0.0) + value
self._all_qubits.update(addend._all_qubits)
else:
raise TypeError(f"Cannot add {type(addend)} to QubitPauliOperator.")
return self
def __add__(self, addend: "QubitPauliOperator") -> "QubitPauliOperator":
"""Addition (+) of QubitPauliOperators.
:param addend: The operator to add
:type addend: QubitPauliOperator
:return: Sum operator
:rtype: QubitPauliOperator
"""
summand = copy.deepcopy(self)
summand += addend
return summand
def __imul__(
self, multiplier: Union[float, Expr, "QubitPauliOperator"]
) -> "QubitPauliOperator":
"""In-place multiplication (*=) with QubitPauliOperator or scalar.
Multiply coefficients and terms.
:param multiplier: The operator or scalar to multiply
:type multiplier: Union[QubitPauliOperator, int, float, complex, Expr]
:return: Updated operator (self)
:rtype: QubitPauliOperator
"""
# Handle operator of the same type
if isinstance(multiplier, QubitPauliOperator):
result_terms: dict = dict()
for left_key, left_value in self._dict.items():
for right_key, right_value in multiplier._dict.items():
new_term, bonus_coeff = pauli_string_mult(left_key, right_key)
new_coefficient = bonus_coeff * left_value * right_value
# Update result dict.
if new_term in result_terms:
result_terms[new_term] += new_coefficient
else:
result_terms[new_term] = new_coefficient
self._dict = result_terms
self._all_qubits.update(multiplier._all_qubits)
return self
# Handle scalars.
if isinstance(multiplier, (float, Expr)):
for key in self._dict:
self[key] *= multiplier
return self
# Invalid multiplier type
raise TypeError(f"Cannot multiply QubitPauliOperator with {type(multiplier)}")
def __mul__(
self, multiplier: Union[float, Expr, "QubitPauliOperator"]
) -> "QubitPauliOperator":
"""Multiplication (*) by QubitPauliOperator or scalar.
:param multiplier: The scalar to multiply by
:type multiplier: Union[int, float, complex, Expr, QubitPauliOperator]
:return: Product operator
:rtype: QubitPauliOperator
"""
product = copy.deepcopy(self)
product *= multiplier
return product
def __rmul__(self, multiplier: CoeffTypeAccepted) -> "QubitPauliOperator":
"""Multiplication (*) by a scalar.
We only define __rmul__ for scalars because left multiply is
queried as default behaviour, and is used for
QubitPauliOperator*QubitPauliOperator.
:param multiplier: The scalar to multiply by
:type multiplier: Union[int, float, complex, Expr]
:return: Product operator
:rtype: QubitPauliOperator
"""
return self.__mul__(_coeff_convert(multiplier))
@property
def all_qubits(self) -> set[Qubit]:
"""
:return: The set of all qubits the operator ranges over (including qubits
that were provided explicitly as identities)
:rtype: Set[Qubit]
"""
return self._all_qubits
[docs]
def subs(self, symbol_dict: dict[Symbol, complex]) -> None:
"""Substitutes any matching symbols in the QubitPauliOperator.
:param symbol_dict: A dictionary of symbols to fixed values.
:type symbol_dict: Dict[Symbol, complex]
"""
for key, value in self._dict.items():
self._dict[key] = value.subs(symbol_dict)
[docs]
def to_list(self) -> list[dict[str, Any]]:
"""Generate a list serialized representation of QubitPauliOperator,
suitable for writing to JSON.
:return: JSON serializable list of dictionaries.
:rtype: List[Dict[str, Any]]
"""
ret: list[dict[str, Any]] = []
for k, v in self._dict.items():
try:
coeff = complex_to_list(complex(v))
except TypeError:
assert isinstance(Expr(v), Expr)
coeff = str(v)
ret.append(
{
"string": k.to_list(),
"coefficient": coeff,
}
)
return ret
[docs]
@classmethod
def from_list(cls, pauli_list: list[dict[str, Any]]) -> "QubitPauliOperator":
"""Construct a QubitPauliOperator from a serializable JSON list format,
as returned by QubitPauliOperator.to_list()
:return: New QubitPauliOperator instance.
:rtype: QubitPauliOperator
"""
def get_qps(obj: dict[str, Any]) -> QubitPauliString:
return QubitPauliString.from_list(obj["string"])
def get_coeff(obj: dict[str, Any]) -> Expr:
coeff = obj["coefficient"]
if type(coeff) is str:
return _coeff_convert(coeff)
return _coeff_convert(list_to_complex(coeff))
return QubitPauliOperator({get_qps(obj): get_coeff(obj) for obj in pauli_list})
[docs]
def to_sparse_matrix(self, qubits: list[Qubit] | int | None = None) -> "csc_matrix":
"""Represents the sparse operator as a dense operator under the ordering
scheme specified by ``qubits``, and generates the corresponding matrix.
- When ``qubits`` is an explicit list, the qubits are ordered with
``qubits[0]`` as the most significant qubit for indexing into the matrix.
- If ``None``, then no padding qubits are introduced and we use the ILO-BE
convention, e.g. ``Qubit("a", 0)`` is more significant than
``Qubit("a", 1)`` or ``Qubit("b")``.
- Giving a number specifies the number of qubits to use in the final
operator, treated as sequentially indexed from 0 in the default register
(padding with identities as necessary) and ordered by ILO-BE so
``Qubit(0)`` is the most significant.
:param qubits: Sequencing of qubits in the matrix, either as an explicit
list, number of qubits to pad to, or infer from the operator.
Defaults to None
:type qubits: Union[List[Qubit], int, None], optional
:return: A sparse matrix representation of the operator.
:rtype: csc_matrix
"""
if qubits is None:
qubits_ = sorted(list(self._all_qubits))
return sum(
complex(coeff) * pauli.to_sparse_matrix(qubits_)
for pauli, coeff in self._dict.items()
)
return sum(
complex(coeff) * pauli.to_sparse_matrix(qubits)
for pauli, coeff in self._dict.items()
)
[docs]
def dot_state(
self, state: np.ndarray, qubits: list[Qubit] | None = None
) -> np.ndarray:
"""Applies the operator to the given state, mapping qubits to indexes
according to ``qubits``.
- When ``qubits`` is an explicit list, the qubits are ordered with
``qubits[0]`` as the most significant qubit for indexing into ``state``.
- If ``None``, qubits sequentially indexed from 0 in the default register
and ordered by ILO-BE so ``Qubit(0)`` is the most significant.
:param state: The initial statevector
:type state: numpy.ndarray
:param qubits: Sequencing of qubits in ``state``, if not mapped to the
default register. Defaults to None
:type qubits: Union[List[Qubit], None], optional
:return: The dot product of the operator with the statevector
:rtype: numpy.ndarray
"""
if qubits:
product_sum = sum(
complex(coeff) * pauli.dot_state(state, qubits)
for pauli, coeff in self._dict.items()
)
else:
product_sum = sum(
complex(coeff) * pauli.dot_state(state)
for pauli, coeff in self._dict.items()
)
return product_sum if isinstance(product_sum, numpy.ndarray) else state
[docs]
def state_expectation(
self, state: np.ndarray, qubits: list[Qubit] | None = None
) -> complex:
"""Calculates the expectation value of the given statevector with respect
to the operator, mapping qubits to indexes according to ``qubits``.
- When ``qubits`` is an explicit list, the qubits are ordered with
``qubits[0]`` as the most significant qubit for indexing into ``state``.
- If ``None``, qubits sequentially indexed from 0 in the default register
and ordered by ILO-BE so ``Qubit(0)`` is the most significant.
:param state: The initial statevector
:type state: numpy.ndarray
:param qubits: Sequencing of qubits in ``state``, if not mapped to the
default register. Defaults to None
:type qubits: Union[List[Qubit], None], optional
:return: The expectation value of the statevector and operator
:rtype: complex
"""
if qubits:
return sum(
complex(coeff) * pauli.state_expectation(state, qubits)
for pauli, coeff in self._dict.items()
)
return sum(
complex(coeff) * pauli.state_expectation(state)
for pauli, coeff in self._dict.items()
)
[docs]
def compress(self, abs_tol: float = 1e-10) -> None:
"""Substitutes all free symbols in the QubitPauliOperator with
1, and then removes imaginary and real components which have
magnitudes below the tolerance. If the resulting expression is
0, the term is removed entirely.
Warning: This methods assumes significant expression structure
is known a priori, and is best suited to operators which have
simple product expressions, such as excitation operators for
VQE ansätze and digital quantum simulation. Otherwise, it may
remove terms relevant to computation. Each expression is of
the form :math:`f(a_1,a_2,\\ldots,a_n)` for some symbols
:math:`a_i`. :math:`|f(a_1,a_2,\\ldots,a_n)|` is assumed to
monotonically increase in both real and imaginary components
for all :math:`a_i \\in [0, 1]`.
:param abs_tol: The threshold below which to remove values.
:type abs_tol: float
"""
to_delete = []
for key, value in self._dict.items():
placeholder = value.subs({s: 1 for s in value.free_symbols})
if abs(re(placeholder)) <= abs_tol:
if abs(im(placeholder)) <= abs_tol:
to_delete.append(key)
else:
self._dict[key] = im(value) * 1j
elif abs(im(placeholder)) <= abs_tol:
self._dict[key] = re(value)
for key in to_delete:
del self._dict[key]
def _collect_qubits(self) -> None:
self._all_qubits: set[Qubit] = set()
for key in self._dict.keys():
for q in key.map.keys():
self._all_qubits.add(q)