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

Add Open graph class #191

Merged
merged 40 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
793bcd5
Add OpenGraph class for graphical compilation
wlcsm Jul 26, 2024
b2f05d4
Add OpenGraph to docs
wlcsm Jul 30, 2024
878173b
Add Open Graph tests
wlcsm Jul 30, 2024
0a30cfa
Implement suggested changes on PR
wlcsm Jul 30, 2024
1cac345
Add PyZX requirement into dev requirements
wlcsm Aug 3, 2024
49adc00
Use enum for measurement planes
wlcsm Aug 3, 2024
37666d6
Skip tests if pyzx isn't installed
wlcsm Aug 4, 2024
a2f2687
Add conversion between patterns and Open graphs
wlcsm Aug 4, 2024
a8af272
Extract PyZX code into separate file
wlcsm Aug 4, 2024
47249b8
Update graphix/open_graph.py
wlcsm Aug 5, 2024
13ad960
Improve docstrings on pattern methods
wlcsm Aug 5, 2024
46da2aa
Rename open_graph.py to opengraph.py
wlcsm Aug 5, 2024
f642748
Change docs for measurement angle
wlcsm Aug 5, 2024
6b738e4
Improve Measurement class's comparisons
wlcsm Aug 5, 2024
bf1b701
Simplify graph equality operation
wlcsm Aug 7, 2024
b3a779f
Simplify internal datastructure for OpenGraph
wlcsm Aug 7, 2024
7728c0d
Improve code quality
wlcsm Aug 8, 2024
5e7b2aa
Use Mapping for the iterface for extensibility
wlcsm Aug 8, 2024
b689ab0
Add warning for PyZX version
wlcsm Aug 15, 2024
fe5551f
Remove classmethod for OpenGraph
wlcsm Aug 15, 2024
63fa2a4
Add comments for clarity
wlcsm Aug 15, 2024
2f37c63
Fix lints
wlcsm Aug 15, 2024
f0df750
Implement changes for random circuit testing
wlcsm Aug 18, 2024
f36de11
Implement changes for random circuit testing
wlcsm Aug 18, 2024
1ba8eb3
Implement changes for random circuit testing
wlcsm Aug 18, 2024
7e30c5d
Implement changes for random circuit testing
wlcsm Aug 18, 2024
056614e
Implement changes for random circuit testing
wlcsm Aug 18, 2024
d5ffb77
Implement changes for random circuit testing
wlcsm Aug 18, 2024
22ce7bd
Implement changes for random circuit testing
wlcsm Aug 18, 2024
cc15e93
No need to reset the random seed
wlcsm Aug 18, 2024
e7f2297
Fix formatting
wlcsm Aug 18, 2024
d68e913
Remove qasm files
wlcsm Aug 18, 2024
35d5367
Add type annotations
wlcsm Aug 20, 2024
ab1c18e
Add type annotations
wlcsm Aug 20, 2024
aa87098
Highlight that inputs/outputs are ordered
wlcsm Aug 20, 2024
e4cbb41
Simplify
wlcsm Aug 20, 2024
e7a7dcb
Add type annotations
wlcsm Aug 20, 2024
6e155dd
Avoid consuming the iterator
wlcsm Aug 21, 2024
dd725cf
Switch from NamedTuple to dataclass for validation
wlcsm Aug 21, 2024
71472ca
Check open graphs are close, not equal
wlcsm Aug 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/source/open_graph.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Open Graph
======================

:mod:`graphix.opengraph` module
+++++++++++++++++++++++++++++

This module defines classes for defining MBQC patterns as Open Graphs.

.. currentmodule:: graphix.opengraph

.. autoclass:: OpenGraph

.. autoclass:: Measurement
123 changes: 123 additions & 0 deletions graphix/opengraph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Provides a class for open graphs."""

from __future__ import annotations

import math
from typing import TYPE_CHECKING, NamedTuple

import networkx as nx

from graphix.generator import generate_from_graph

if TYPE_CHECKING:
from graphix.pattern import Pattern
from graphix.pauli import Plane

Check warning on line 14 in graphix/opengraph.py

View check run for this annotation

Codecov / codecov/patch

graphix/opengraph.py#L13-L14

Added lines #L13 - L14 were not covered by tests


class Measurement(NamedTuple):
"""An MBQC measurement.

:param angle: the angle of the measurement. Should be between [0, 2)
:param plane: the measurement plane
"""

angle: float
plane: Plane

def isclose(self, other: Measurement, rel_tol=1e-09, abs_tol=0.0) -> bool:
wlcsm marked this conversation as resolved.
Show resolved Hide resolved
"""Compares if two measurements have the same plane and their angles
are close.

Example
-------
>>> from graphix.opengraph import Measurement
>>> from graphix.pauli import Plane
>>> Measurement(0.0, Plane.XY).isclose(Measurement(0.0, Plane.XY))
True
>>> Measurement(0.0, Plane.XY).isclose(Measurement(0.0, Plane.YZ))
False
>>> Measurement(0.1, Plane.XY).isclose(Measurement(0.0, Plane.XY))
False
"""
return math.isclose(self.angle, other.angle, rel_tol=rel_tol, abs_tol=abs_tol) and self.plane == other.plane


class OpenGraph(NamedTuple):
"""Open graph contains the graph, measurement, and input and output
nodes. This is the graph we wish to implement deterministically

:param inside: the underlying graph state
:param measurements: a dictionary whose key is the ID of a node and the
value is the measurement at that node
:param inputs: a set of IDs of the nodes that are inputs to the graph
:param outputs: a set of IDs of the nodes that are outputs of the graph

Example
-------
>>> import networkx as nx
>>> from graphix.opengraph import OpenGraph, Measurement
>>>
>>> inside_graph = nx.Graph([(0, 1), (1, 2), (2, 0)])
>>>
>>> measurements = {i: Measurement(0.5 * i, Plane.XY) for i in range(2)}
>>> inputs = [0]
>>> outputs = [2]
>>> og = OpenGraph(inside_graph, measurements, inputs, outputs)
"""

inside: nx.Graph
measurements: dict[int, Measurement]
inputs: list[int]
outputs: list[int]

def __eq__(self, other) -> bool:
wlcsm marked this conversation as resolved.
Show resolved Hide resolved
"""Checks the two open graphs are equal

This doesn't check they are equal up to an isomorphism"""

if not nx.utils.graphs_equal(self.inside, other.inside):
return False

Check warning on line 79 in graphix/opengraph.py

View check run for this annotation

Codecov / codecov/patch

graphix/opengraph.py#L79

Added line #L79 was not covered by tests

if self.inputs != other.inputs or self.outputs != other.outputs:
return False

Check warning on line 82 in graphix/opengraph.py

View check run for this annotation

Codecov / codecov/patch

graphix/opengraph.py#L82

Added line #L82 was not covered by tests

if set(self.measurements.keys()) != set(other.measurements.keys()):
return False

Check warning on line 85 in graphix/opengraph.py

View check run for this annotation

Codecov / codecov/patch

graphix/opengraph.py#L85

Added line #L85 was not covered by tests

return all(m.isclose(other.measurements[node]) for node, m in self.measurements.items())

@classmethod
def from_pattern(cls, pattern: Pattern) -> OpenGraph:
"""Initialises an `OpenGraph` object based on the resource-state graph
associated with the measurement pattern."""
g = nx.Graph()
nodes, edges = pattern.get_graph()
g.add_nodes_from(nodes)
g.add_edges_from(edges)

inputs = pattern.input_nodes
outputs = pattern.output_nodes

meas_planes = pattern.get_meas_plane()
meas_angles = pattern.get_angles()
meas = {node: Measurement(meas_angles[node], meas_planes[node]) for node in meas_angles}

return cls(g, meas, inputs, outputs)

def to_pattern(self) -> Pattern:
"""Converts the `OpenGraph` into a `Pattern`.

Will raise an exception if the open graph does not have flow, gflow, or
Pauli flow.
The pattern will be generated using maximally-delayed flow.
"""

g = self.inside.copy()
inputs = self.inputs
outputs = self.outputs
meas = self.measurements

angles = {node: m.angle for node, m in meas.items()}
planes = {node: m.plane for node, m in meas.items()}

return generate_from_graph(g, angles, inputs, outputs, planes)
178 changes: 178 additions & 0 deletions graphix/pyzx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""Functionality for converting between OpenGraphs and PyZX

These functions are held in their own file rather than including them in the
OpenGraph class because we want PyZX to be an optional dependency.
"""

from __future__ import annotations

import warnings

import networkx as nx
import pyzx as zx
wlcsm marked this conversation as resolved.
Show resolved Hide resolved

from graphix.opengraph import Measurement, OpenGraph
from graphix.pauli import Plane


def to_pyzx_graph(og: OpenGraph) -> zx.graph.base.BaseGraph:
"""Return a PyZX graph corresponding to the the open graph.

wlcsm marked this conversation as resolved.
Show resolved Hide resolved
Example
-------
>>> import networkx as nx
>>> g = nx.Graph([(0, 1), (1, 2)])
>>> inputs = [0]
>>> outputs = [2]
>>> measurements = {0: Measurement(0, Plane.XY), 1: Measurement(1, Plane.YZ)}
>>> og = OpenGraph(g, measurements, inputs, outputs)
>>> reconstructed_pyzx_graph = og.to_pyzx_graph()
"""
# check pyzx availability and version
try:
import pyzx as zx
except ModuleNotFoundError as e:
msg = "Cannot find pyzx (optional dependency)."
raise RuntimeError(msg) from e

Check warning on line 36 in graphix/pyzx.py

View check run for this annotation

Codecov / codecov/patch

graphix/pyzx.py#L34-L36

Added lines #L34 - L36 were not covered by tests
if zx.__version__ != "0.8.0":
warnings.warn(

Check warning on line 38 in graphix/pyzx.py

View check run for this annotation

Codecov / codecov/patch

graphix/pyzx.py#L38

Added line #L38 was not covered by tests
"`to_pyzx_graph` is guaranteed to work only with pyzx==0.8.0 due to possible breaking changes in `pyzx`.",
stacklevel=1,
)
g = zx.Graph()
wlcsm marked this conversation as resolved.
Show resolved Hide resolved

# Add vertices into the graph and set their type
def add_vertices(n: int, ty: zx.VertexType) -> list[zx.VertexType]:
verts = g.add_vertices(n)
for vert in verts:
g.set_type(vert, ty)

return verts

# Add input boundary nodes
in_verts = add_vertices(len(og.inputs), zx.VertexType.BOUNDARY)
g.set_inputs(in_verts)

# Add nodes for internal Z spiders - not including the phase gadgets
body_verts = add_vertices(len(og.inside), zx.VertexType.Z)

# Add nodes for the phase gadgets. In OpenGraph we don't store the
# effect as a seperate node, it is instead just stored in the
# "measurement" attribute of the node it measures.
x_meas = [i for i, m in og.measurements.items() if m.plane == Plane.YZ]
x_meas_verts = add_vertices(len(x_meas), zx.VertexType.Z)

out_verts = add_vertices(len(og.outputs), zx.VertexType.BOUNDARY)
g.set_outputs(out_verts)

# Maps a node's ID in the Open Graph to it's corresponding node ID in
# the PyZX graph and vice versa.
map_to_og = dict(zip(body_verts, og.inside.nodes()))
map_to_pyzx = {v: i for i, v in map_to_og.items()}

# Open Graph's don't have boundary nodes, so we need to connect the
# input and output Z spiders to their corresponding boundary nodes in
# pyzx.
for pyzx_index, og_index in zip(in_verts, og.inputs):
g.add_edge((pyzx_index, map_to_pyzx[og_index]))
for pyzx_index, og_index in zip(out_verts, og.outputs):
g.add_edge((pyzx_index, map_to_pyzx[og_index]))

og_edges = og.inside.edges()
pyzx_edges = [(map_to_pyzx[a], map_to_pyzx[b]) for a, b in og_edges]
g.add_edges(pyzx_edges, zx.EdgeType.HADAMARD)
wlcsm marked this conversation as resolved.
Show resolved Hide resolved

# Add the edges between the Z spiders in the graph body
for og_index, meas in og.measurements.items():
# If it's an X measured node, then we handle it in the next loop
if meas.plane == Plane.XY:
g.set_phase(map_to_pyzx[og_index], meas.angle)

# Connect the X measured vertices
for og_index, pyzx_index in zip(x_meas, x_meas_verts):
g.add_edge((map_to_pyzx[og_index], pyzx_index), zx.EdgeType.HADAMARD)
g.set_phase(pyzx_index, og.measurements[og_index].angle)

Check warning on line 94 in graphix/pyzx.py

View check run for this annotation

Codecov / codecov/patch

graphix/pyzx.py#L93-L94

Added lines #L93 - L94 were not covered by tests

return g


def from_pyzx_graph(g: zx.graph.base.BaseGraph) -> OpenGraph:
"""Constructs an Optyx Open Graph from a PyZX graph.

This method may add additional nodes to the graph so that it adheres
with the definition of an OpenGraph. For instance, if the final node on
a qubit is measured, it will add two nodes behind it so that no output
nodes are measured to satisfy the requirements of an open graph.
.. warning::
works with `pyzx==0.8.0` (see `requirements-dev.txt`). Other versions may not be compatible due to breaking changes in `pyzx`
Example
-------
>>> import pyzx as zx
>>> from graphix.opengraph import OpenGraph
>>> circ = zx.qasm("qreg q[2]; h q[1]; cx q[0], q[1]; h q[1];")
>>> g = circ.to_graph()
>>> og = OpenGraph.from_pyzx_graph(g)
"""
zx.simplify.to_graph_like(g)
wlcsm marked this conversation as resolved.
Show resolved Hide resolved

measurements = {}
inputs = g.inputs()
outputs = g.outputs()

g_nx = nx.Graph(g.edges())

# We need to do this since the full reduce simplification can
# leave either hadamard or plain wires on the inputs and outputs
for inp in g.inputs():
nbrs = list(g.neighbors(inp))
wlcsm marked this conversation as resolved.
Show resolved Hide resolved
et = g.edge_type((nbrs[0], inp))

if et == zx.EdgeType.SIMPLE:
g_nx.remove_node(inp)
inputs = [i if i != inp else nbrs[0] for i in inputs]

for out in g.outputs():
nbrs = list(g.neighbors(out))
et = g.edge_type((nbrs[0], out))

if et == zx.EdgeType.SIMPLE:
g_nx.remove_node(out)
outputs = [o if o != out else nbrs[0] for o in outputs]

# Turn all phase gadgets into measurements
# Since we did a full reduce, any node that isn't an input or output
# node and has only one neighbour is definitely a phase gadget.
nodes = list(g_nx.nodes())
for v in nodes:
if v in inputs or v in outputs:
continue

nbrs = list(g.neighbors(v))
if len(nbrs) == 1:
measurements[nbrs[0]] = Measurement(float(g.phase(v)), Plane.YZ)
g_nx.remove_node(v)

Check warning on line 153 in graphix/pyzx.py

View check run for this annotation

Codecov / codecov/patch

graphix/pyzx.py#L152-L153

Added lines #L152 - L153 were not covered by tests

next_id = max(g_nx.nodes) + 1

# Since outputs can't be measured, we need to add an extra two nodes
# in to counter it
for out in outputs:
if g.phase(out) == 0:
continue

g_nx.add_edges_from([(out, next_id), (next_id, next_id + 1)])
measurements[next_id] = Measurement(0, Plane.XY)

outputs = [o if o != out else next_id + 1 for o in outputs]
next_id += 2

# Add the phase to all XY measured nodes
for v in g_nx.nodes:
if v in outputs or v in measurements:
continue

# g.phase() may be a fractions.Fraction object, but Measurement
# expects a float
measurements[v] = Measurement(float(g.phase(v)), Plane.XY)

return OpenGraph(g_nx, measurements, inputs, outputs)
3 changes: 3 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ tox
qiskit>=1.0
qiskit-aer
rustworkx

# Optional dependency. Pinned due to version changes often being incompatible
pyzx==0.8.0
31 changes: 31 additions & 0 deletions tests/circuits/adder_n4.qasm
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
OPENQASM 2.0;
include "qelib1.inc";
qreg q[4];
creg c[4];
x q[0];
x q[1];
h q[3];
cx q[2],q[3];
t q[0];
t q[1];
t q[2];
tdg q[3];
cx q[0],q[1];
cx q[2],q[3];
cx q[3],q[0];
cx q[1],q[2];
cx q[0],q[1];
cx q[2],q[3];
tdg q[0];
tdg q[1];
tdg q[2];
t q[3];
cx q[0],q[1];
cx q[2],q[3];
s q[3];
cx q[3],q[0];
h q[3];
measure q[0] -> c[0];
measure q[1] -> c[1];
measure q[2] -> c[2];
measure q[3] -> c[3];
Loading
Loading