Skip to content
Open
38 changes: 2 additions & 36 deletions pyquil/paulis.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,43 +423,9 @@ def from_list(cls, terms_list: List[Tuple[str, int]], coefficient: float = 1.0)
@classmethod
def from_compact_str(cls, str_pauli_term: str) -> "PauliTerm":
"""Construct a PauliTerm from the result of str(pauli_term)"""
# split into str_coef, str_op at first '*'' outside parenthesis
try:
str_coef, str_op = re.split(r"\*(?![^(]*\))", str_pauli_term, maxsplit=1)
except ValueError:
raise ValueError(
"Could not separate the pauli string into "
f"coefficient and operator. {str_pauli_term} does"
" not match <coefficient>*<operator>"
)

# parse the coefficient into either a float or complex
str_coef = str_coef.replace(" ", "")
try:
coef: Union[float, complex] = float(str_coef)
except ValueError:
try:
coef = complex(str_coef)
except ValueError:
raise ValueError(f"Could not parse the coefficient {str_coef}")

op = sI() * coef
if str_op == "I":
assert isinstance(op, PauliTerm)
return op

# parse the operator
str_op = re.sub(r"\*", "", str_op)
if not re.match(r"^(([XYZ])(\d+))+$", str_op):
raise ValueError(
fr"Could not parse operator string {str_op}. It should match ^(([XYZ])(\d+))+$"
)

for factor in re.finditer(r"([XYZ])(\d+)", str_op):
op *= cls(factor.group(1), int(factor.group(2)))
from .paulis_parser import parse_pauli_str

assert isinstance(op, PauliTerm)
return op
return parse_pauli_str(str_pauli_term)

def pauli_string(self, qubits: Optional[Iterable[int]] = None) -> str:
"""
Expand Down
115 changes: 115 additions & 0 deletions pyquil/paulis_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from functools import lru_cache
from typing import Callable, Tuple, Union

from lark import Lark, Token, Transformer, Tree, v_args

from pyquil.paulis import PauliSum, PauliTerm, sI, sX, sY, sZ


PAULI_GRAMMAR = r"""
?start: pauli_term
| start "-" start -> pauli_sub_pauli
| start "+" start -> pauli_add_pauli

?pauli_term: operator_term
| coefficient "*" pauli_term -> op_term_with_coefficient
| coefficient pauli_term -> op_term_with_coefficient
| pauli_term "*" coefficient -> coefficient_with_op_term
| pauli_term "*" pauli_term -> op_term_with_op_term
| pauli_term pauli_term -> op_term_with_op_term

?operator_term: operator_with_index
| "I" -> op_i

?operator_with_index: operator_taking_index INT -> op_with_index

?operator_taking_index: "X" -> op_x
| "Y" -> op_y
| "Z" -> op_z

?coefficient: NUMBER
| complex -> to_complex

?complex: "(" SIGNED_NUMBER "+" NUMBER "j" ")"

%import common.INT
%import common.SIGNED_NUMBER
%import common.NUMBER
%import common.WS_INLINE

%ignore WS_INLINE

"""


@v_args(inline=True)
class PauliTree(Transformer): # type: ignore
""" An AST Transformer to convert the given string into a tree """

def op_x(self) -> Callable[[int], PauliTerm]:
return sX

def op_y(self) -> Callable[[int], PauliTerm]:
return sY

def op_z(self) -> Callable[[int], PauliTerm]:
return sZ

def op_i(self) -> PauliTerm:
return sI()

def op_with_index(self, op: Callable[[int], PauliTerm], index: Token) -> PauliTerm:
return op(int(index.value))

def op_term_with_coefficient(self, coeff: Union[complex, Tree], op: PauliTerm) -> PauliTerm:
coeff = coeff if isinstance(coeff, complex) else float(coeff.value)
return coeff * op

def coefficient_with_op_term(self, op: PauliTerm, coeff: Union[complex, Tree]) -> PauliTerm:
return self.op_term_with_coefficient(coeff, op)

def op_term_with_op_term(self, first: PauliTerm, second: PauliTerm) -> PauliTerm:
return first * second

def to_complex(self, *args: Tuple[Tree, Tree]) -> complex:
assert len(args[0].children) == 2, "Parsing error"
real, imag = args[0].children
return float(real.value) + float(imag.value) * 1j

def pauli_mul_pauli(self, first: PauliTerm, second: PauliTerm) -> Union[PauliTerm, PauliSum]:
return first * second

def pauli_add_pauli(self, first: PauliTerm, second: PauliTerm) -> Union[PauliTerm, PauliSum]:
return first + second


@lru_cache(maxsize=None)
def pauli_parser() -> Lark:
"""
This returns the parser object for Pauli compact string
parsing, however it will only ever instantiate one parser
per python process, and will re-use it for all subsequent
calls to `from_compact_str`.

:return: An instance of a Lark parser for Pauli strings
"""
return Lark(PAULI_GRAMMAR, parser="lalr", transformer=PauliTree())


def parse_pauli_str(data: str) -> Union[Tree, PauliTerm]:
"""
Examples of Pauli Strings:

=> (1.5 + 0.5j)*X0*Z2+.7*Z1
=> "(1.5 + 0.5j)*X0*Z2+.7*I"

A Pauli Term is a product of Pauli operators operating on
different qubits - the operator can be one of "X", "Y", "Z", "I",
including an index (ie. the qubit index such as 0, 1 or 2) and
the coefficient multiplying the operator, eg. `1.5 * Z1`.

Note: "X", "Y" and "Z" are always followed by the qubit index,
but "I" being the identity is not.
"""
parser = pauli_parser()
return parser.parse(data)
13 changes: 5 additions & 8 deletions pyquil/tests/test_paulis.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import numpy as np
import pytest
from lark import UnexpectedCharacters, UnexpectedToken

from pyquil.gates import RX, RZ, CNOT, H, X, PHASE
from pyquil.paulis import (
Expand Down Expand Up @@ -749,7 +750,7 @@ def test_str():


def test_from_str():
with pytest.raises(ValueError):
with pytest.raises(UnexpectedCharacters):
PauliTerm.from_compact_str("1*A0→1*Z0")


Expand All @@ -774,15 +775,11 @@ def test_qubit_validation():

def test_pauli_term_from_str():
# tests that should _not_ fail are in test_pauli_sum_from_str
with pytest.raises(ValueError):
PauliTerm.from_compact_str("X0")
with pytest.raises(ValueError):
with pytest.raises(UnexpectedToken):
PauliTerm.from_compact_str("10")
with pytest.raises(ValueError):
PauliTerm.from_compact_str("1.0X0")
with pytest.raises(ValueError):
with pytest.raises(UnexpectedCharacters):
PauliTerm.from_compact_str("(1.0+9i)*X0")
with pytest.raises(ValueError):
with pytest.raises(UnexpectedCharacters, match="Expecting:"):
PauliTerm.from_compact_str("(1.0+0j)*A0")


Expand Down
119 changes: 119 additions & 0 deletions pyquil/tests/test_paulis_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from lark import UnexpectedCharacters, UnexpectedToken
from pytest import raises

from pyquil.paulis import (
sI,
sX,
sY,
sZ,
)
from pyquil.paulis_parser import parse_pauli_str


def test_pauli_sums_parsing():
result = parse_pauli_str("(1.5 + 0.5j)*X0*Z2")
assert result == (1.5 + 0.5j) * sX(0) * sZ(2)

# the `.compact_str()` method on PauliSum can also return this
result = parse_pauli_str("(1.5+0.5j)*X0Z2")
assert result == (1.5 + 0.5j) * sX(0) * sZ(2)

result = parse_pauli_str("(1.5 + 0.5j)*X0 + (1.0 + 0.25j)*Z2")
assert result == (1.5 + 0.5j) * sX(0) + (1.0 + 0.25j) * sZ(2)

result = parse_pauli_str("(1.5 + 0.5j)*X0 + 1.5 * Z2")
assert result == (1.5 + 0.5j) * sX(0) + 1.5 * sZ(2)

result = parse_pauli_str("(1.5 + 0.5j)*X0*Z2+.7*I")
assert result == (1.5 + 0.5j) * sX(0) * sZ(2) + 0.7 * sI(0)

# check sums of length one
result = parse_pauli_str("1*Y0*Y1")
assert result == 1 * sY(0) * sY(1)

# Here we reverse the multiplication of .7 and I
result = parse_pauli_str("(1.5 + 0.5j)*X0*Z2+I * .7")
assert result == (1.5 + 0.5j) * sX(0) * sZ(2) + 0.7 * sI(0)

# ...and check the simplification...
result = parse_pauli_str("1*Y0*X0 + (0+1j)*Z0 + 2*Y1")
assert result == 2 * sY(1)

# test case from PauliSum docstring
result = parse_pauli_str("0.5*X0 + (0.5+0j)*Z2")
assert result == 0.5 * sX(0) + (0.5 + 0j) * sZ(2)

# test case from test_setting using _generate_random_paulis
result = parse_pauli_str("(-0.5751426877923431+0j)*Y0X1X3")
assert result == (-0.5751426877923431 + 0j) * sY(0) * sX(1) * sX(3)


def test_complex_number_parsing():
assert parse_pauli_str("(1+0j) * X1") == (1.0 + 0j) * sX(1)
assert parse_pauli_str("(1.1 + 0.1j) * Z2") == (1.1 + 0.1j) * sZ(2)
assert parse_pauli_str("(0 + 1j) * Y1") == (0 + 1j) * sY(1)

with raises(UnexpectedCharacters, match="Expecting:"):
# If someone uses 'i' instead of 'j' we get a useful message
# in an UnexpectedToken exception stating what's acceptable
parse_pauli_str("(1 + 0i) * X1")

with raises(UnexpectedToken, match="Expected one of:"):
# If someone accidentally uses '*' instead of '+' in the
# complex number, we get a useful error message
parse_pauli_str("(1 * 0.25j) * X1")


def test_pauli_terms_parsing():
# A PauliTerm consists of: operator, index, coefficient,
# where the index and coefficient are sometimes optional
# Eg. in the simplest case we just have I, which is fine
assert parse_pauli_str("I") == sI(0)

# ...but just having the operator without an index is
# *not* ok for X, Y or Z...
with raises(UnexpectedToken):
parse_pauli_str("X")
with raises(UnexpectedToken):
parse_pauli_str("Y")
with raises(UnexpectedToken):
parse_pauli_str("Z")

# ...these operators require an index to be included as well
assert parse_pauli_str("X0") == sX(0)
assert parse_pauli_str("X1") == sX(1)
assert parse_pauli_str("Y0") == sY(0)
assert parse_pauli_str("Y1") == sY(1)
assert parse_pauli_str("Z0") == sZ(0)
assert parse_pauli_str("Z1") == sZ(1)
assert parse_pauli_str("Z2") == sZ(2)

# The other optional item for a pauli term is the coefficient,
# which in the simplest case could just be this:
result = parse_pauli_str("1.5 * Z1")
assert result == 1.5 * sZ(1)

# the simple cases should also be the same as a complex coefficient
# with 1. and 0j
result = parse_pauli_str("Z1")
assert result == (1.0 + 0j) * sZ(1)

# we also need to support short-hand versions of floats like this:
result = parse_pauli_str(".5 * Z0")
assert result == 0.5 * sZ(0)

# ...and just to check it parses the same without whitespace
result = parse_pauli_str(".5*X0")
assert result == 0.5 * sX(0)

# we can now support even shorter notation like this
result = parse_pauli_str(".5X0")
assert result == 0.5 * sX(0)

# Obviously the coefficients can also be complex, so we need to
# support this:
result = parse_pauli_str("(0 + 1j) * Z0")
assert result == (0 + 1j) * sZ(0)

result = parse_pauli_str("(1.0 + 0j) * X0")
assert result == (1.0 + 0j) * sX(0)