|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""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 == []
|
|
|
|
|
|
@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):
|
|
|
|
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
|
|
|
|
|
|
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
|
|
|
|
|
|
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 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]:
|
|
"""
|
|
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:
|
|
"""
|
|
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
|
|
|