Source code for braket.quantum_information.pauli_sum

# Copyright 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 numbers
from collections.abc import Iterable
from itertools import combinations, starmap

from braket.circuits.observable import Observable, StandardObservable
from braket.circuits.observables import I, Sum, TensorProduct, X, Y, Z
from braket.quantum_information.pauli_string import PauliString

_OBSERVABLE_TO_FACTOR = {I: "I", X: "X", Y: "Y", Z: "Z"}


[docs] class PauliSum: """A weighted sum of Pauli strings.""" def __init__(self, terms: Iterable[tuple[numbers.Number, str | PauliString]] = ()): """Initializes a ``PauliSum``. Args: terms (Iterable[tuple[numbers.Number, str | PauliString]]): Pairs of coefficient and Pauli string. """ self._terms: dict[str, numbers.Number] = {} for coefficient, pauli_string in terms: self._add_term(coefficient, PauliString(pauli_string)) self._all_terms_commute = self._compute_all_terms_commute() @property def all_terms_commute(self) -> bool: """bool: Whether all terms in the sum commute with each other.""" return self._all_terms_commute
[docs] @classmethod def from_list(cls, terms: Iterable[tuple[numbers.Number, str | PauliString]]) -> PauliSum: """Builds a ``PauliSum`` from a list of weighted Pauli strings.""" return cls(terms)
@property def terms(self) -> tuple[tuple[numbers.Number, PauliString], ...]: """tuple[tuple[numbers.Number, PauliString], ...]: The weighted Pauli terms.""" return tuple( (coefficient, PauliString(pauli)) for pauli, coefficient in self._terms.items() ) @property def qubit_count(self) -> int: """int: The number of qubits in the largest Pauli string term.""" if not self._terms: return 0 return max(PauliString(pauli).qubit_count for pauli in self._terms)
[docs] def to_list(self) -> list[tuple[numbers.Number, str]]: """Returns a list representation of the weighted Pauli strings.""" return [(coefficient, pauli) for pauli, coefficient in self._terms.items()]
[docs] def to_sum(self) -> Sum: """Converts the weighted Pauli strings into a circuit ``Sum`` observable.""" if not self._terms: raise ValueError("Cannot convert an empty PauliSum to Sum") observables = [] for coefficient, pauli in self.terms: observables.append(coefficient * pauli.to_unsigned_observable(include_trivial=True)) return Sum(observables)
[docs] def commutes_with(self, other: PauliSum | PauliString | str) -> bool: """Returns whether all terms commute with ``other``.""" other_sum = self._coerce(other) if not self.all_terms_commute or not other_sum.all_terms_commute: return False return all( self._pauli_strings_commute(left, right) for _, left in self.terms for _, right in other_sum.terms )
[docs] def is_self_commuting(self) -> bool: """Returns whether all terms in this sum commute with each other.""" return self._all_terms_commute
[docs] @classmethod def from_sum(cls, observable_sum: Sum) -> PauliSum: """Builds a ``PauliSum`` from a circuit ``Sum`` observable.""" if not isinstance(observable_sum, Sum): raise TypeError("Expected a Sum observable") return cls(cls._term_from_observable(observable) for observable in observable_sum.summands)
def __add__(self, other: PauliSum | PauliString | str) -> PauliSum: other_sum = self._coerce(other) return PauliSum((*self.terms, *other_sum.terms)) def __radd__(self, other: PauliSum | PauliString | str) -> PauliSum: return self + other def __sub__(self, other: PauliSum | PauliString | str) -> PauliSum: return self + (-1 * self._coerce(other)) def __rsub__(self, other: PauliSum | PauliString | str) -> PauliSum: return self._coerce(other) + (-self) def __neg__(self) -> PauliSum: return -1 * self def __mul__(self, other: numbers.Number | PauliString | str) -> PauliSum: if isinstance(other, numbers.Number): return PauliSum((coefficient * other, pauli) for coefficient, pauli in self.terms) pauli = PauliString(other) qubit_count = max(self.qubit_count, pauli.qubit_count) right = self._pad_pauli_string(pauli, qubit_count) return PauliSum( (coefficient, self._pad_pauli_string(term, qubit_count).dot(right)) for coefficient, term in self.terms ) def __rmul__(self, other: numbers.Number | PauliString | str) -> PauliSum: if isinstance(other, numbers.Number): return self * other pauli = PauliString(other) qubit_count = max(self.qubit_count, pauli.qubit_count) left = self._pad_pauli_string(pauli, qubit_count) return PauliSum( (coefficient, left.dot(self._pad_pauli_string(term, qubit_count))) for coefficient, term in self.terms ) def __contains__(self, item: str | PauliString) -> bool: _, pauli = self._canonical_term(PauliString(item)) return pauli in self._terms def __getitem__(self, item: int) -> tuple[numbers.Number, PauliString]: return self.terms[item] def __iter__(self): return iter(self.terms) def __len__(self) -> int: return len(self._terms) def __eq__(self, other: PauliSum) -> bool: if not isinstance(other, PauliSum): return False return self._terms == other._terms def __repr__(self) -> str: return f"PauliSum({self.to_list()})" def _add_term(self, coefficient: numbers.Number, pauli_string: PauliString) -> None: if not isinstance(coefficient, numbers.Number): raise TypeError("PauliSum coefficients must be numbers") coefficient, pauli = self._canonical_term(pauli_string, coefficient) if coefficient == 0: return new_coefficient = self._terms.get(pauli, 0) + coefficient if new_coefficient == 0: self._terms.pop(pauli, None) else: self._terms[pauli] = new_coefficient def _compute_all_terms_commute(self) -> bool: if len(self._terms) <= 1: return True paulis = [PauliString(pauli) for pauli in self._terms] return all(starmap(self._pauli_strings_commute, combinations(paulis, 2))) @staticmethod def _canonical_term( pauli_string: PauliString, coefficient: numbers.Number = 1 ) -> tuple[numbers.Number, str]: factors = ["I"] * pauli_string.qubit_count for qubit in range(pauli_string.qubit_count): factors[qubit] = "IXYZ"[pauli_string[qubit]] return coefficient * pauli_string.phase, f"+{''.join(factors)}" @staticmethod def _coerce(other: PauliSum | PauliString | str) -> PauliSum: if isinstance(other, PauliSum): return other return PauliSum([(1, other)]) @staticmethod def _pauli_strings_commute(left: PauliString, right: PauliString) -> bool: anticommuting_factors = 0 qubit_count = max(left.qubit_count, right.qubit_count) for qubit in range(qubit_count): left_factor = left[qubit] if qubit < left.qubit_count else 0 right_factor = right[qubit] if qubit < right.qubit_count else 0 if left_factor and right_factor and left_factor != right_factor: anticommuting_factors += 1 return anticommuting_factors % 2 == 0 @staticmethod def _pad_pauli_string(pauli_string: PauliString, qubit_count: int) -> PauliString: if pauli_string.qubit_count == qubit_count: return pauli_string factors = ["I"] * qubit_count for qubit in range(pauli_string.qubit_count): factors[qubit] = "IXYZ"[pauli_string[qubit]] sign = "-" if pauli_string.phase < 0 else "+" return PauliString(f"{sign}{''.join(factors)}") @staticmethod def _term_from_observable(observable: Observable) -> tuple[numbers.Number, str]: coefficient = observable.coefficient unscaled = observable._unscaled() if isinstance(unscaled, StandardObservable): factors = PauliSum._factors_from_standard(unscaled) elif isinstance(unscaled, TensorProduct): factors = PauliSum._factors_from_tensor_product(unscaled) else: raise TypeError(f"Unsupported observable type {type(observable).__name__}") return coefficient, f"+{''.join(factors)}" @staticmethod def _factors_from_standard(observable: StandardObservable) -> list[str]: factor = PauliSum._factor_from_standard(observable) if observable.targets: factors = ["I"] * (max(observable.targets) + 1) factors[int(observable.targets[0])] = factor return factors return [factor] @staticmethod def _factors_from_tensor_product(observable: TensorProduct) -> list[str]: if observable.targets: factors = ["I"] * (max(observable.targets) + 1) for factor, target in zip(observable.factors, observable.targets, strict=True): factors[int(target)] = PauliSum._factor_from_standard(factor) return factors return [PauliSum._factor_from_standard(factor) for factor in observable.factors] @staticmethod def _factor_from_standard(observable: StandardObservable) -> str: for observable_type, factor in _OBSERVABLE_TO_FACTOR.items(): if isinstance(observable, observable_type): return factor raise TypeError(f"Unsupported observable factor {type(observable).__name__}")
PauliStringSum = PauliSum