Skip to content

Commit 332f4b2

Browse files
Fix zero-qubit Pauli label strings (#9726)
It was previously possible to construct a zero-qubit Pauli operator using the array form (`Pauli(([], []))`), or by empty-slicing an existing Pauli (`Pauli("IXZ")[[]]`). This commit completes the set by making labels with no qubits work as well, for consistency. Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 3284ea0 commit 332f4b2

File tree

5 files changed

+40
-43
lines changed

5 files changed

+40
-43
lines changed

qiskit/quantum_info/operators/symplectic/pauli.py

+8-37
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ class initialization (``Pauli('-iXYZ')``). A ``Pauli`` object can be
147147
# Set the max Pauli string size before truncation
148148
__truncate__ = 50
149149

150-
_VALID_LABEL_PATTERN = re.compile(r"^[+-]?1?[ij]?[IXYZ]+$")
150+
_VALID_LABEL_PATTERN = re.compile(r"(?P<coeff>[+-]?1?[ij]?)(?P<pauli>[IXYZ]*)")
151+
_CANONICAL_PHASE_LABEL = {"": 0, "-i": 1, "-": 2, "i": 3}
151152

152153
def __init__(self, data=None, x=None, *, z=None, label=None):
153154
"""Initialize the Pauli.
@@ -613,17 +614,15 @@ def _from_label(label):
613614
Raises:
614615
QiskitError: if Pauli string is not valid.
615616
"""
616-
if Pauli._VALID_LABEL_PATTERN.match(label) is None:
617+
match_ = Pauli._VALID_LABEL_PATTERN.fullmatch(label)
618+
if match_ is None:
617619
raise QiskitError(f'Pauli string label "{label}" is not valid.')
618-
619-
# Split string into coefficient and Pauli
620-
pauli, coeff = _split_pauli_label(label)
621-
622-
# Convert coefficient to phase
623-
phase = 0 if not coeff else _phase_from_label(coeff)
620+
phase = Pauli._CANONICAL_PHASE_LABEL[
621+
(match_["coeff"] or "").replace("1", "").replace("+", "").replace("j", "i")
622+
]
624623

625624
# Convert to Symplectic representation
626-
pauli_bytes = np.frombuffer(pauli.encode("ascii"), dtype=np.uint8)[::-1]
625+
pauli_bytes = np.frombuffer(match_["pauli"].encode("ascii"), dtype=np.uint8)[::-1]
627626
ys = pauli_bytes == ord("Y")
628627
base_x = np.logical_or(pauli_bytes == ord("X"), ys).reshape(1, -1)
629628
base_z = np.logical_or(pauli_bytes == ord("Z"), ys).reshape(1, -1)
@@ -698,33 +697,5 @@ def _from_circuit(cls, instr):
698697
return ret._z, ret._x, ret._phase
699698

700699

701-
# ---------------------------------------------------------------------
702-
# Label parsing helper functions
703-
# ---------------------------------------------------------------------
704-
705-
706-
def _split_pauli_label(label):
707-
"""Split Pauli label into unsigned group label and coefficient label"""
708-
span = re.search(r"[IXYZ]+", label).span()
709-
pauli = label[span[0] :]
710-
coeff = label[: span[0]]
711-
if span[1] != len(label):
712-
invalid = set(re.sub(r"[IXYZ]+", "", label[span[0] :]))
713-
raise QiskitError(
714-
f"Pauli string contains invalid characters {invalid} ∉ ['I', 'X', 'Y', 'Z']"
715-
)
716-
return pauli, coeff
717-
718-
719-
def _phase_from_label(label):
720-
"""Return the phase from a label"""
721-
# Returns None if label is invalid
722-
label = label.replace("+", "", 1).replace("1", "", 1).replace("j", "i", 1)
723-
phases = {"": 0, "-i": 1, "-": 2, "i": 3}
724-
if label not in phases:
725-
raise QiskitError(f"Invalid Pauli phase label '{label}'")
726-
return phases[label]
727-
728-
729700
# Update docstrings for API docs
730701
generate_apidocs(Pauli)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed an edge case in the construction of :class:`.Pauli` instances; a string with an optional
5+
phase and no qubits is now a valid label, making an operator with no qubits (such as
6+
``Pauli("-i")``). This was already possible when using the array forms, or empty slices.
7+
Fixed `#9720 <https://github.com/Qiskit/qiskit-terra/issues/9720>`__.

test/python/opflow/test_op_construction.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
Zero,
6464
)
6565
from qiskit.quantum_info import Operator, Pauli, Statevector
66-
from qiskit.quantum_info.operators.symplectic.pauli import _phase_from_label, _split_pauli_label
6766

6867
# pylint: disable=invalid-name
6968

@@ -1242,9 +1241,7 @@ def pauli_group_labels(nq, full_group=True):
12421241

12431242
def operator_from_label(label):
12441243
"""Construct operator from full Pauli group label"""
1245-
pauli, coeff = _split_pauli_label(label)
1246-
coeff = (-1j) ** _phase_from_label(coeff)
1247-
return coeff * Operator.from_label(pauli)
1244+
return Operator(Pauli(label))
12481245

12491246

12501247
@ddt

test/python/primitives/test_estimator.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,7 @@ def test_validate_observables(self, obsevables, expected):
696696
"""Test obsevables standardization."""
697697
self.assertEqual(BaseEstimator._validate_observables(obsevables), expected)
698698

699-
@data(None, "ERROR", "")
699+
@data(None, "ERROR")
700700
def test_qiskit_error(self, observables):
701701
"""Test qiskit error if invalid input."""
702702
with self.assertRaises(QiskitError):

test/python/quantum_info/operators/symplectic/test_pauli.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
"""Tests for Pauli operator class."""
1616

17+
import re
1718
import unittest
1819
import itertools as it
1920
from functools import lru_cache
@@ -41,7 +42,19 @@
4142

4243
from qiskit.quantum_info.random import random_clifford, random_pauli
4344
from qiskit.quantum_info.operators import Pauli, Operator
44-
from qiskit.quantum_info.operators.symplectic.pauli import _split_pauli_label, _phase_from_label
45+
46+
LABEL_REGEX = re.compile(r"(?P<coeff>[+-]?1?[ij]?)(?P<pauli>[IXYZ]*)")
47+
PHASE_MAP = {"": 0, "-i": 1, "-": 2, "i": 3}
48+
49+
50+
def _split_pauli_label(label):
51+
match_ = LABEL_REGEX.fullmatch(label)
52+
return match_["pauli"], match_["coeff"]
53+
54+
55+
def _phase_from_label(label):
56+
coeff = LABEL_REGEX.fullmatch(label)["coeff"] or ""
57+
return PHASE_MAP[coeff.replace("+", "").replace("1", "").replace("j", "i")]
4558

4659

4760
@lru_cache(maxsize=8)
@@ -462,6 +475,15 @@ def test_barrier_delay_sim(self):
462475
value = Pauli(circ)
463476
self.assertEqual(value, target)
464477

478+
@data(("", 0), ("-", 2), ("i", 3), ("-1j", 1))
479+
@unpack
480+
def test_zero_qubit_pauli_construction(self, label, phase):
481+
"""Test that Paulis of zero qubits can be constructed."""
482+
expected = Pauli(label + "X")[0:0] # Empty slice from a 1q Pauli, which becomes phaseless
483+
expected.phase = phase
484+
test = Pauli(label)
485+
self.assertEqual(expected, test)
486+
465487

466488
if __name__ == "__main__":
467489
unittest.main()

0 commit comments

Comments
 (0)