Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose cutoff tolerances in Z2Symmetries #7598

Merged
merged 14 commits into from
Feb 17, 2022
18 changes: 17 additions & 1 deletion qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def __init__(
sq_paulis: List[Pauli],
sq_list: List[int],
tapering_values: Optional[List[int]] = None,
tol: float = 1e-14,
):
"""
Args:
Expand All @@ -98,6 +99,8 @@ def __init__(
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 or complex parts of a coefficient.
Cryoris marked this conversation as resolved.
Show resolved Hide resolved

Raises:
OpflowError: Invalid paulis
"""
Expand All @@ -122,6 +125,17 @@ def __init__(
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 or complex parts of a coefficient."""
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
return self._tol

@tol.setter
def tol(self, value):
"""Set the tolerance threshold for ignoring real or complex parts of a coefficient."""
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
self._tol = value

@property
def symmetries(self):
Expand Down Expand Up @@ -391,7 +405,9 @@ def _taper(self, op: PauliSumOp, curr_tapering_values: List[int]) -> OperatorBas
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 = SparsePauliOp.from_list(pauli_list).simplify(0.0)
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
spo = spo.chop(self.tol)
z2_symmetries = self.copy()
z2_symmetries.tapering_values = curr_tapering_values

Expand Down
40 changes: 40 additions & 0 deletions qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,46 @@ def simplify(self, atol=None, rtol=None):
x = self.paulis.x[non_zero_indexes]
z = self.paulis.z[non_zero_indexes]
coeffs = coeffs[non_zero]

return SparsePauliOp(
PauliList.from_symplectic(z, x), coeffs, ignore_pauli_phase=True, copy=False
)

def chop(self, tol=1e-14):
"""Remove real and imaginary parts of the coefficient that are close to 0.
Cryoris marked this conversation as resolved.
Show resolved Hide resolved

For example, the operator representing ``1+1e-17j X + 1e-17 Y`` with a tolerance larger
than ``1e-17`` will be reduced to ``1 X`` whereas :meth:`.SparsePauliOp.simplify` would
return ``1+1e-17j X``.

Args:
tol (float): The absolute tolerance to check whether a real or imaginary part is 0.

Returns:
SparsePauliOp: This operator with chopped coefficients.
"""
# Remove real (resp. imaginary) parts that are close to 0. This part for instance
# truncates 1+1e-17j to 1.0. Note that all coefficients that are left at this point
# are guaranteed to have a value above tolerance due to the previous is_zero check.
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
realpart_nonzero = np.logical_not(np.isclose(self.coeffs.real, 0, atol=tol))
imagpart_nonzero = np.logical_not(np.isclose(self.coeffs.imag, 0, atol=tol))
Cryoris marked this conversation as resolved.
Show resolved Hide resolved

remaining_indices = np.logical_or(realpart_nonzero, imagpart_nonzero)
remaining_real = realpart_nonzero[remaining_indices]
remaining_imag = imagpart_nonzero[remaining_indices]

if not np.any(remaining_indices):
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
x = np.zeros((1, self.num_qubits), dtype=bool)
z = np.zeros((1, self.num_qubits), dtype=bool)
coeffs = np.array([0j], dtype=complex)
else:
coeffs = np.zeros(sum(remaining_indices), dtype=complex)
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
coeffs[remaining_real] += self.coeffs.real[realpart_nonzero]
coeffs[remaining_imag] += 1j * self.coeffs.imag[imagpart_nonzero]
Cryoris marked this conversation as resolved.
Show resolved Hide resolved

x = self.paulis.x[remaining_indices]
z = self.paulis.z[remaining_indices]

return SparsePauliOp(
PauliList.from_symplectic(z, x), coeffs, ignore_pauli_phase=True, copy=False
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
features:
- |
The :class:`~qiskit.opflow.Z2Symmetries` class now exposes the threshold
tolerances used to chop small real- and imaginary parts of coefficients.
With this one can control how the coefficients of the tapered operator are
simplified. For example::

from qiskit.opflow import Z2Symmetries
from qiskit.quantum_info import Pauli

z2_symmetries = Z2Symmetries(
symmetries=[Pauli("IIZI"), Pauli("IZIZ"), Pauli("ZIII")],
sq_paulis=[Pauli("IIXI"), Pauli("IIIX"), Pauli("XIII")],
sq_list=[1, 0, 3],
tapering_values=[1, -1, -1],
tol=1e-10,
)

Per default coefficients are chopped with a tolerance of ``tol=1e-14``.
- |
Add a :meth:`qiskit.quantum_info.SparsePauliOp.chop` method that truncates real and
imaginary parts of coefficients individually. This is different to
:meth:`qiskit.quantum_info.SparsePauliOp.simplify` which removes a coefficient only if
the absolute value is close to 0. For example::

>>> from qiskit.quantum_info import SparsePauliOp
>>> op = SparsePauliOp(["X", "Y", "Z"], coeffs=[1+1e-17j, 1e-17+1j, 1e-17])
>>> op.simplify()
SparsePauliOp(['X', 'Y'],
coeffs=[1.e+00+1.e-17j, 1.e-17+1.e+00j])
>>> op.chop()
SparsePauliOp(['X', 'Y'],
coeffs=[1.+0.j, 0.+1.j])

Note that the chop method does not accumulate the coefficents of the same Paulis.
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
24 changes: 24 additions & 0 deletions test/python/opflow/test_z2_symmetries.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,27 @@ def test_taper_empty_operator(self):
tapered_op = z2_symmetries.taper(empty_op)
expected_op = PauliSumOp.from_list([("I", 0.0)])
self.assertEqual(tapered_op, expected_op)

def test_truncate_tapered_op(self):
"""Test setting cutoff tolerances for the tapered operator works."""
qubit_op = PauliSumOp.from_list(
[
("II", -1.0537076071291125),
("IZ", 0.393983679438514),
("ZI", -0.39398367943851387),
("ZZ", -0.01123658523318205),
("XX", 0.1812888082114961),
]
)
z2_symmetries = Z2Symmetries.find_Z2_symmetries(qubit_op)
z2_symmetries.tol = 0.2 # removes the X part of the tapered op which is < 0.2

tapered_op = z2_symmetries.taper(qubit_op)[1]
primitive = SparsePauliOp.from_list(
[
("I", -1.0424710218959303),
("Z", -0.7879673588770277),
]
)
expected_op = TaperedPauliSumOp(primitive, z2_symmetries)
self.assertEqual(tapered_op, expected_op)
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,24 @@ def test_simplify2(self, num_qubits, num_adds):
np.testing.assert_array_equal(spp_op.paulis.phase, np.zeros(spp_op.size))
np.testing.assert_array_equal(simplified_op.paulis.phase, np.zeros(simplified_op.size))

def test_chop(self):
"""Test chop, which individually truncates real and imaginary parts of the coeffs."""
eps = 1e-10
op = SparsePauliOp(
["I", "Z", "X", "Y"], coeffs=[eps + 1j * eps, 1 + 1j * eps, eps + 1j, 1 + 1j]
)
simplified = op.chop(tol=eps)
expected_coeffs = [1, 1j, 1 + 1j]
self.assertListEqual(simplified.coeffs.tolist(), expected_coeffs)

def test_chop_all(self):
"""Test that chop returns an identity operator with coeff 0 if all coeffs are chopped."""
eps = 1e-10
op = SparsePauliOp(["X", "Z"], coeffs=[eps, eps])
simplified = op.chop(tol=eps)
expected = SparsePauliOp(["I"], coeffs=[0.0])
self.assertEqual(simplified, expected)

Cryoris marked this conversation as resolved.
Show resolved Hide resolved
@combine(num_qubits=[1, 2, 3, 4], num_ops=[1, 2, 3, 4])
def test_sum(self, num_qubits, num_ops):
"""Test sum method for {num_qubits} qubits with {num_ops} operators."""
Expand Down