Skip to content

Commit

Permalink
Fix #45: Parameterized measurement patterns
Browse files Browse the repository at this point in the history
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`).
  • Loading branch information
thierry-martinez committed May 30, 2024
1 parent ec4c582 commit a5e16de
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 0 deletions.
56 changes: 56 additions & 0 deletions graphix/pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
56 changes: 56 additions & 0 deletions tests/test_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import numpy as np
import pytest
import sympy

import tests.random_circuit as rc
from graphix.pattern import CommandNode, Pattern
Expand Down Expand Up @@ -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"""
Expand Down

0 comments on commit a5e16de

Please sign in to comment.