herutriana44's picture
First Commit
b7d9967 verified
raw
history blame contribute delete
No virus
22.1 kB
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021, 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""TaperedPauliSumOp Class and Z2Symmetries"""
import itertools
import logging
from copy import deepcopy
from typing import Dict, List, Optional, Union, cast
import numpy as np
from qiskit.circuit import ParameterExpression
from qiskit.opflow.exceptions import OpflowError
from qiskit.opflow.list_ops import ListOp
from qiskit.opflow.operator_base import OperatorBase
from qiskit.opflow.primitive_ops.pauli_op import PauliOp
from qiskit.opflow.primitive_ops.pauli_sum_op import PauliSumOp
from qiskit.opflow.utils import commutator
from qiskit.quantum_info import Pauli, SparsePauliOp
from qiskit.utils.deprecation import deprecate_func
logger = logging.getLogger(__name__)
class TaperedPauliSumOp(PauliSumOp):
"""Deprecated: Class for PauliSumOp after tapering"""
@deprecate_func(
since="0.24.0",
additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.",
)
def __init__(
self,
primitive: SparsePauliOp,
z2_symmetries: "Z2Symmetries",
coeff: Union[complex, ParameterExpression] = 1.0,
) -> None:
"""
Args:
primitive: The SparsePauliOp which defines the behavior of the underlying function.
z2_symmetries: Z2 symmetries which the Operator has.
coeff: A coefficient multiplying the primitive.
Raises:
TypeError: invalid parameters.
"""
super().__init__(primitive, coeff)
if not isinstance(z2_symmetries, Z2Symmetries):
raise TypeError(
f"Argument parameter z2_symmetries must be Z2Symmetries, not {type(z2_symmetries)}"
)
self._z2_symmetries = z2_symmetries
@property
def z2_symmetries(self) -> "Z2Symmetries":
"""
Z2 symmetries which the Operator has.
Returns:
The Z2 Symmetries.
"""
return self._z2_symmetries
@property
def settings(self) -> Dict:
"""Return operator settings."""
return {
"primitive": self._primitive,
"z2_symmetries": self._z2_symmetries,
"coeff": self._coeff,
}
def assign_parameters(self, param_dict: dict) -> OperatorBase:
pauli_sum = PauliSumOp(self.primitive, self.coeff)
return pauli_sum.assign_parameters(param_dict)
class Z2Symmetries:
"""Deprecated: Z2 Symmetries"""
@deprecate_func(
since="0.24.0",
additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.",
)
def __init__(
self,
symmetries: List[Pauli],
sq_paulis: List[Pauli],
sq_list: List[int],
tapering_values: Optional[List[int]] = None,
tol: float = 1e-14,
):
"""
Args:
symmetries: the list of Pauli objects representing the Z_2 symmetries
sq_paulis: the list of single - qubit Pauli objects to construct the
Clifford operators
sq_list: the list of support of the single-qubit Pauli objects used to build
the Clifford operators
tapering_values: values determines the sector.
tol: Tolerance threshold for ignoring real and complex parts of a coefficient.
Raises:
OpflowError: Invalid paulis
"""
if len(symmetries) != len(sq_paulis):
raise OpflowError(
"Number of Z2 symmetries has to be the same as number of single-qubit pauli x."
)
if len(sq_paulis) != len(sq_list):
raise OpflowError(
"Number of single-qubit pauli x has to be the same as length of single-qubit list."
)
if tapering_values is not None:
if len(sq_list) != len(tapering_values):
raise OpflowError(
"The length of single-qubit list has "
"to be the same as length of tapering values."
)
self._symmetries = symmetries
self._sq_paulis = sq_paulis
self._sq_list = sq_list
self._tapering_values = tapering_values
self._tol = tol
@property
def tol(self):
"""Tolerance threshold for ignoring real and complex parts of a coefficient."""
return self._tol
@tol.setter
def tol(self, value):
"""Set the tolerance threshold for ignoring real and complex parts of a coefficient."""
self._tol = value
@property
def symmetries(self):
"""return symmetries"""
return self._symmetries
@property
def sq_paulis(self):
"""returns sq paulis"""
return self._sq_paulis
@property
def cliffords(self) -> List[PauliSumOp]:
"""
Get clifford operators, build based on symmetries and single-qubit X.
Returns:
a list of unitaries used to diagonalize the Hamiltonian.
"""
cliffords = [
(PauliOp(pauli_symm) + PauliOp(sq_pauli)) / np.sqrt(2)
for pauli_symm, sq_pauli in zip(self._symmetries, self._sq_paulis)
]
return cliffords
@property
def sq_list(self):
"""returns sq list"""
return self._sq_list
@property
def tapering_values(self):
"""returns tapering values"""
return self._tapering_values
@tapering_values.setter
def tapering_values(self, new_value):
"""set tapering values"""
self._tapering_values = new_value
@property
def settings(self) -> Dict:
"""Return operator settings."""
return {
"symmetries": self._symmetries,
"sq_paulis": self._sq_paulis,
"sq_list": self._sq_list,
"tapering_values": self._tapering_values,
}
def __str__(self):
ret = ["Z2 symmetries:"]
ret.append("Symmetries:")
for symmetry in self._symmetries:
ret.append(symmetry.to_label())
ret.append("Single-Qubit Pauli X:")
for x in self._sq_paulis:
ret.append(x.to_label())
ret.append("Cliffords:")
for c in self.cliffords:
ret.append(str(c))
ret.append("Qubit index:")
ret.append(str(self._sq_list))
ret.append("Tapering values:")
if self._tapering_values is None:
possible_values = [
str(list(coeff)) for coeff in itertools.product([1, -1], repeat=len(self._sq_list))
]
possible_values = ", ".join(x for x in possible_values)
ret.append(" - Possible values: " + possible_values)
else:
ret.append(str(self._tapering_values))
ret = "\n".join(ret)
return ret
def copy(self) -> "Z2Symmetries":
"""
Get a copy of self.
Returns:
copy
"""
return deepcopy(self)
def is_empty(self) -> bool:
"""
Check the z2_symmetries is empty or not.
Returns:
Empty or not
"""
return self._symmetries == [] or self._sq_paulis == [] or self._sq_list == []
# pylint: disable=invalid-name
@classmethod
def find_Z2_symmetries(cls, operator: PauliSumOp) -> "Z2Symmetries":
"""
Finds Z2 Pauli-type symmetries of an Operator.
Returns:
a z2_symmetries object contains symmetries, single-qubit X, single-qubit list.
"""
pauli_symmetries = []
sq_paulis = []
sq_list = []
stacked_paulis = []
if operator.is_zero():
logger.info("Operator is empty.")
return cls([], [], [], None)
for pauli in operator:
stacked_paulis.append(
np.concatenate(
(pauli.primitive.paulis.x[0], pauli.primitive.paulis.z[0]), axis=0
).astype(int)
)
stacked_matrix = np.array(np.stack(stacked_paulis))
symmetries = _kernel_F2(stacked_matrix)
if not symmetries:
logger.info("No symmetry is found.")
return cls([], [], [], None)
stacked_symmetries = np.stack(symmetries)
symm_shape = stacked_symmetries.shape
for row in range(symm_shape[0]):
pauli_symmetries.append(
Pauli(
(
stacked_symmetries[row, : symm_shape[1] // 2],
stacked_symmetries[row, symm_shape[1] // 2 :],
)
)
)
stacked_symm_del = np.delete(stacked_symmetries, row, axis=0)
for col in range(symm_shape[1] // 2):
# case symmetries other than one at (row) have Z or I on col qubit
Z_or_I = True
for symm_idx in range(symm_shape[0] - 1):
if not (
stacked_symm_del[symm_idx, col] == 0
and stacked_symm_del[symm_idx, col + symm_shape[1] // 2] in (0, 1)
):
Z_or_I = False
if Z_or_I:
if (
stacked_symmetries[row, col] == 1
and stacked_symmetries[row, col + symm_shape[1] // 2] == 0
) or (
stacked_symmetries[row, col] == 1
and stacked_symmetries[row, col + symm_shape[1] // 2] == 1
):
sq_paulis.append(
Pauli((np.zeros(symm_shape[1] // 2), np.zeros(symm_shape[1] // 2)))
)
sq_paulis[row].z[col] = False
sq_paulis[row].x[col] = True
sq_list.append(col)
break
# case symmetries other than one at (row) have X or I on col qubit
X_or_I = True
for symm_idx in range(symm_shape[0] - 1):
if not (
stacked_symm_del[symm_idx, col] in (0, 1)
and stacked_symm_del[symm_idx, col + symm_shape[1] // 2] == 0
):
X_or_I = False
if X_or_I:
if (
stacked_symmetries[row, col] == 0
and stacked_symmetries[row, col + symm_shape[1] // 2] == 1
) or (
stacked_symmetries[row, col] == 1
and stacked_symmetries[row, col + symm_shape[1] // 2] == 1
):
sq_paulis.append(
Pauli((np.zeros(symm_shape[1] // 2), np.zeros(symm_shape[1] // 2)))
)
sq_paulis[row].z[col] = True
sq_paulis[row].x[col] = False
sq_list.append(col)
break
# case symmetries other than one at (row) have Y or I on col qubit
Y_or_I = True
for symm_idx in range(symm_shape[0] - 1):
if not (
(
stacked_symm_del[symm_idx, col] == 1
and stacked_symm_del[symm_idx, col + symm_shape[1] // 2] == 1
)
or (
stacked_symm_del[symm_idx, col] == 0
and stacked_symm_del[symm_idx, col + symm_shape[1] // 2] == 0
)
):
Y_or_I = False
if Y_or_I:
if (
stacked_symmetries[row, col] == 0
and stacked_symmetries[row, col + symm_shape[1] // 2] == 1
) or (
stacked_symmetries[row, col] == 1
and stacked_symmetries[row, col + symm_shape[1] // 2] == 0
):
sq_paulis.append(
Pauli((np.zeros(symm_shape[1] // 2), np.zeros(symm_shape[1] // 2)))
)
sq_paulis[row].z[col] = True
sq_paulis[row].x[col] = True
sq_list.append(col)
break
return cls(pauli_symmetries, sq_paulis, sq_list, None)
def convert_clifford(self, operator: PauliSumOp) -> OperatorBase:
"""This method operates the first part of the tapering.
It converts the operator by composing it with the clifford unitaries defined in the current
symmetry.
Args:
operator: to-be-tapered operator
Returns:
:class:`PauliSumOp` corresponding to the converted operator.
Raises:
OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty
"""
if not self._symmetries or not self._sq_paulis or not self._sq_list:
raise OpflowError(
"Z2 symmetries, single qubit pauli and single qubit list cannot be empty."
)
if not operator.is_zero():
for clifford in self.cliffords:
operator = cast(PauliSumOp, clifford @ operator @ clifford)
operator = operator.reduce(atol=0)
return operator
def taper_clifford(self, operator: PauliSumOp) -> OperatorBase:
"""This method operates the second part of the tapering.
This function assumes that the input operators have already been transformed using
:meth:`convert_clifford`. The redundant qubits due to the symmetries are dropped and
replaced by their two possible eigenvalues.
The `tapering_values` will be stored into the resulted operator for a record.
Args:
operator: Partially tapered operator resulting from a call to :meth:`convert_clifford`
Returns:
If tapering_values is None: [:class:`PauliSumOp`]; otherwise, :class:`PauliSumOp`
Raises:
OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty
"""
if not self._symmetries or not self._sq_paulis or not self._sq_list:
raise OpflowError(
"Z2 symmetries, single qubit pauli and single qubit list cannot be empty."
)
# If the operator is zero then we can skip the following. We still need to taper the
# operator to reduce its size i.e. the number of qubits so for example 0*"IIII" could
# taper to 0*"II" when symmetries remove two qubits.
if self._tapering_values is None:
tapered_ops_list = [
self._taper(operator, list(coeff))
for coeff in itertools.product([1, -1], repeat=len(self._sq_list))
]
tapered_ops: OperatorBase = ListOp(tapered_ops_list)
else:
tapered_ops = self._taper(operator, self._tapering_values)
return tapered_ops
def taper(self, operator: PauliSumOp) -> OperatorBase:
"""
Taper an operator based on the z2_symmetries info and sector defined by `tapering_values`.
The `tapering_values` will be stored into the resulted operator for a record.
The tapering is a two-step algorithm which first converts the operator into a
:class:`PauliSumOp` with same eigenvalues but where some qubits are only acted upon
with the Pauli operators I or X.
The number M of these redundant qubits is equal to the number M of identified symmetries.
The second step of the reduction consists in replacing these qubits with the possible
eigenvalues of the corresponding Pauli X, giving 2^M new operators with M less qubits.
If an eigenvalue sector was previously identified for the solution, then this reduces to
1 new operator with M less qubits.
Args:
operator: the to-be-tapered operator
Returns:
If tapering_values is None: [:class:`PauliSumOp`]; otherwise, :class:`PauliSumOp`
Raises:
OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty
"""
if not self._symmetries or not self._sq_paulis or not self._sq_list:
raise OpflowError(
"Z2 symmetries, single qubit pauli and single qubit list cannot be empty."
)
converted_ops = self.convert_clifford(operator)
tapered_ops = self.taper_clifford(converted_ops)
return tapered_ops
def _taper(self, op: PauliSumOp, curr_tapering_values: List[int]) -> OperatorBase:
pauli_list = []
for pauli_term in op:
coeff_out = pauli_term.primitive.coeffs[0]
for idx, qubit_idx in enumerate(self._sq_list):
if (
pauli_term.primitive.paulis.z[0, qubit_idx]
or pauli_term.primitive.paulis.x[0, qubit_idx]
):
coeff_out = curr_tapering_values[idx] * coeff_out
z_temp = np.delete(pauli_term.primitive.paulis.z[0].copy(), np.asarray(self._sq_list))
x_temp = np.delete(pauli_term.primitive.paulis.x[0].copy(), np.asarray(self._sq_list))
pauli_list.append((Pauli((z_temp, x_temp)).to_label(), coeff_out))
spo = SparsePauliOp.from_list(pauli_list).simplify(atol=0.0)
spo = spo.chop(self.tol)
z2_symmetries = self.copy()
z2_symmetries.tapering_values = curr_tapering_values
return TaperedPauliSumOp(spo, z2_symmetries)
def consistent_tapering(self, operator: PauliSumOp) -> OperatorBase:
"""
Tapering the `operator` with the same manner of how this tapered operator
is created. i.e., using the same Cliffords and tapering values.
Args:
operator: the to-be-tapered operator
Returns:
The tapered operator
Raises:
OpflowError: The given operator does not commute with the symmetry
"""
for symmetry in self._symmetries:
commutator_op = cast(PauliSumOp, commutator(operator, PauliOp(symmetry)))
if not commutator_op.is_zero():
raise OpflowError(
"The given operator does not commute with the symmetry, can not taper it."
)
return self.taper(operator)
def __eq__(self, other: object) -> bool:
"""
Overload `==` operation to evaluate equality between Z2Symmetries.
Args:
other: The `Z2Symmetries` to compare to self.
Returns:
A bool equal to the equality of self and other.
"""
if not isinstance(other, Z2Symmetries):
return False
return (
self.symmetries == other.symmetries
and self.sq_paulis == other.sq_paulis
and self.sq_list == other.sq_list
and self.tapering_values == other.tapering_values
)
def _kernel_F2(matrix_in) -> List[np.ndarray]: # pylint: disable=invalid-name
"""
Computes the kernel of a binary matrix on the binary finite field
Args:
matrix_in (numpy.ndarray): binary matrix
Returns:
The list of kernel vectors
"""
size = matrix_in.shape
kernel = []
matrix_in_id = np.vstack((matrix_in, np.identity(size[1])))
matrix_in_id_ech = (_row_echelon_F2(matrix_in_id.transpose())).transpose()
for col in range(size[1]):
if np.array_equal(
matrix_in_id_ech[0 : size[0], col], np.zeros(size[0])
) and not np.array_equal(matrix_in_id_ech[size[0] :, col], np.zeros(size[1])):
kernel.append(matrix_in_id_ech[size[0] :, col])
return kernel
def _row_echelon_F2(matrix_in) -> np.ndarray: # pylint: disable=invalid-name
"""
Computes the row Echelon form of a binary matrix on the binary finite field
Args:
matrix_in (numpy.ndarray): binary matrix
Returns:
Matrix_in in Echelon row form
"""
size = matrix_in.shape
for i in range(size[0]):
pivot_index = 0
for j in range(size[1]):
if matrix_in[i, j] == 1:
pivot_index = j
break
for k in range(size[0]):
if k != i and matrix_in[k, pivot_index] == 1:
matrix_in[k, :] = np.mod(matrix_in[k, :] + matrix_in[i, :], 2)
matrix_out_temp = deepcopy(matrix_in)
indices = []
matrix_out = np.zeros(size)
for i in range(size[0] - 1):
if np.array_equal(matrix_out_temp[i, :], np.zeros(size[1])):
indices.append(i)
for row in np.sort(indices)[::-1]:
matrix_out_temp = np.delete(matrix_out_temp, (row), axis=0)
matrix_out[0 : size[0] - len(indices), :] = matrix_out_temp
matrix_out = matrix_out.astype(int)
return matrix_out