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

Pauli string parsing #1292

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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)