diff --git a/pyquil/paulis.py b/pyquil/paulis.py index 4168c2157..0343722bf 100644 --- a/pyquil/paulis.py +++ b/pyquil/paulis.py @@ -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 *" - ) - - # 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: """ diff --git a/pyquil/paulis_parser.py b/pyquil/paulis_parser.py new file mode 100644 index 000000000..530b7a58d --- /dev/null +++ b/pyquil/paulis_parser.py @@ -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) diff --git a/pyquil/tests/test_paulis.py b/pyquil/tests/test_paulis.py index c8f819e85..c226cc05a 100644 --- a/pyquil/tests/test_paulis.py +++ b/pyquil/tests/test_paulis.py @@ -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 ( @@ -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") @@ -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") diff --git a/pyquil/tests/test_paulis_parser.py b/pyquil/tests/test_paulis_parser.py new file mode 100644 index 000000000..a7cd78e92 --- /dev/null +++ b/pyquil/tests/test_paulis_parser.py @@ -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)