From a5e16deff4cc7913a4cf24dfe0df45ef5df4df90 Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Thu, 30 May 2024 11:19:59 +0200 Subject: [PATCH] Fix #45: Parameterized measurement patterns This commit is yet another tentative to implement parameterized measurement patterns, to fulfill issue #45 (previous tentative: #68). This commit adds two methods to the class `Pattern`: - `is_parameterized()` returns True if there is at least one measurement angle that is not just an instance of numbers.Number: indeed, a parameterized pattern is a pattern where at least one measurement angle is an expression that is not a number, typically an instance of `sympy.Expr` (but we don't force to choose sympy here). - `subs(variable, substitute)` returns a copy of the pattern where occurrences of the given variable in measurement angles are substituted by the given value. Substitution is performed by calling the method `subs` on measurement angles, if the method exists, which is the case in particular for `sympy.Expr`. If the substitution returns a number, this number is coerced to `float`, to get numbers that implement the full number protocol (in particular, sympy numbers don't implement `cos`). --- graphix/pattern.py | 56 +++++++++++++++++++++++++++++++++++++++++++ tests/test_pattern.py | 56 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/graphix/pattern.py b/graphix/pattern.py index fc4efc46..64fd54fe 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -2,6 +2,7 @@ ref: V. Danos, E. Kashefi and P. Panangaden. J. ACM 54.2 8 (2007) """ +import numbers from copy import deepcopy import networkx as nx @@ -1296,6 +1297,10 @@ def simulate_pattern(self, backend="statevector", **kwargs): .. seealso:: :class:`graphix.simulator.PatternSimulator` """ + if self.is_parameterized(): + raise ValueError( + "Cannot simulate parameterized patterns: the pattern should be instantiated with `subs` first." + ) sim = PatternSimulator(self, backend=backend, **kwargs) state = sim.run() return state @@ -1424,6 +1429,57 @@ def to_qasm3(self, filename): for line in cmd_to_qasm3(command): file.write(line) + def is_parameterized(self) -> bool: + """Return True if there is at least one measurement angle that + is not just an instance of `numbers.Number`. A parameterized + pattern is a pattern where at least one measurement angle is an + expression that is not a number, typically an instance of `sympy.Expr` + (but we don't force to choose `sympy` here). + """ + return any(not isinstance(cmd[3], numbers.Number) for cmd in self if cmd[0] == "M") + + def subs(self, variable, substitute) -> "Pattern": + """Return a copy of the pattern where all occurrences of the + given variable in measurement angles are substituted by the + given value. + + Substitution is performed by calling the method `subs` on + measurement angles, if the method exists, which is the case in + particular for `sympy.Expr`. If the substitution returns a + number, this number is coerced to `float`, to get numbers that + implement the full number protocol (in particular, sympy + numbers don't implement `cos`). + + """ + result = Pattern(input_nodes=self.input_nodes) + for cmd in self: + if cmd[0] == "M": + angle = cmd[3] + try: + # We only require parameterized angles to + # implement the `subs` method: it is the case for + # sympy.Expr, but we may want to use other kinds + # of expressions. + subs = angle.subs + except AttributeError: + subs = None + else: + subs = None + if subs: + new_cmd = cmd.copy() + new_angle = subs(variable, substitute) + if isinstance(new_angle, numbers.Number): + # Coercion to float: sympy numbers do not + # implement the full number protocol. + new_cmd[3] = float(new_angle) + else: + # new_angle is still a parameterized expression. + new_cmd[3] = new_angle + result.add(new_cmd) + else: + result.add(cmd) + return result + class CommandNode: """A node decorated with a distributed command sequence. diff --git a/tests/test_pattern.py b/tests/test_pattern.py index 2308a00a..dfa47701 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -6,6 +6,7 @@ import numpy as np import pytest +import sympy import tests.random_circuit as rc from graphix.pattern import CommandNode, Pattern @@ -295,6 +296,61 @@ def test_get_meas_plane(self) -> None: meas_plane = pattern.get_meas_plane() assert meas_plane == ref_meas_plane + def test_parameter(self) -> None: + pattern = Pattern(input_nodes=[0, 1]) + pattern.add(["M", 0, "XY", 0, [], []]) + # A pattern without parameterized angle is not parameterized. + assert not pattern.is_parameterized() + # Substitution in a pattern without parameterized angle is the identity. + alpha = sympy.Symbol("alpha") + assert list(pattern) == list(pattern.subs(alpha, 0)) + # A pattern without parameterized angle can be simulated. + pattern.simulate_pattern() + pattern.add(["M", 1, "XY", alpha, [], []]) + assert pattern.is_parameterized() + # A parameterized pattern cannot be simulated. + with pytest.raises(ValueError): + pattern.simulate_pattern() + # Parameterized patterns can be substituted, even if some angles are not parameterized. + pattern0 = pattern.subs(alpha, 0) + # If all parameterized angles have been instantiated, the pattern is no longer parameterized. + assert not pattern0.is_parameterized() + assert list(pattern0) == [["M", 0, "XY", 0, [], []], ["M", 1, "XY", 0, [], []]] + # Instantied patterns can be simulated. + pattern0.simulate_pattern() + pattern1 = pattern.subs(alpha, 1) + assert not pattern1.is_parameterized() + assert list(pattern1) == [["M", 0, "XY", 0, [], []], ["M", 1, "XY", 1, [], []]] + pattern1.simulate_pattern() + beta = sympy.Symbol("beta") + pattern.add(["N", 2]) + pattern.add(["M", 2, "XY", beta, [], []]) + # A partially instantiated pattern is still parameterized. + assert pattern.subs(alpha, 2).is_parameterized() + pattern23 = pattern.subs(alpha, 2).subs(beta, 3) + # A full instantiated pattern is no longer parameterized. + assert not pattern23.is_parameterized() + assert list(pattern23) == [ + ["M", 0, "XY", 0, [], []], + ["M", 1, "XY", 2, [], []], + ["N", 2], + ["M", 2, "XY", 3, [], []], + ] + pattern23.simulate_pattern() + # Parameterized angles support expressions. + pattern_beta = pattern.subs(alpha, beta + 1) + assert pattern_beta.is_parameterized() + # Substitution evaluates expressions. + pattern43 = pattern_beta.subs(beta, 3) + assert not pattern43.is_parameterized() + assert list(pattern43) == [ + ["M", 0, "XY", 0, [], []], + ["M", 1, "XY", 4.0, [], []], + ["N", 2], + ["M", 2, "XY", 3.0, [], []], + ] + pattern43.simulate_pattern() + def cp(circuit: Circuit, theta: float, control: int, target: int) -> None: """Controlled rotation gate, decomposed"""