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 18 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
162 changes: 162 additions & 0 deletions graphix/opengraph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""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 collections.abc import Mapping

Check warning on line 13 in graphix/opengraph.py

View check run for this annotation

Codecov / codecov/patch

graphix/opengraph.py#L13

Added line #L13 was not covered by tests

from graphix.pattern import Pattern
from graphix.pauli import Plane

Check warning on line 16 in graphix/opengraph.py

View check run for this annotation

Codecov / codecov/patch

graphix/opengraph.py#L15-L16

Added lines #L15 - L16 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:
EarlMilktea marked this conversation as resolved.
Show resolved Hide resolved
"""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
__inputs: list[int]
__outputs: list[int]
__meas: Mapping[int, Measurement]
wlcsm marked this conversation as resolved.
Show resolved Hide resolved

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 81 in graphix/opengraph.py

View check run for this annotation

Codecov / codecov/patch

graphix/opengraph.py#L81

Added line #L81 was not covered by tests

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

Check warning on line 84 in graphix/opengraph.py

View check run for this annotation

Codecov / codecov/patch

graphix/opengraph.py#L84

Added line #L84 was not covered by tests

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

Check warning on line 87 in graphix/opengraph.py

View check run for this annotation

Codecov / codecov/patch

graphix/opengraph.py#L87

Added line #L87 was not covered by tests

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

def __init__(
self,
inside: nx.Graph,
measurements: Mapping[int, Measurement],
inputs: list[int],
wlcsm marked this conversation as resolved.
Show resolved Hide resolved
outputs: list[int],
) -> None:
"""Constructs a new OpenGraph instance

The inputs() and outputs() methods will preserve the order that was
original given in to this methods.
"""
self._inside = inside
wlcsm marked this conversation as resolved.
Show resolved Hide resolved

if any(node in outputs for node in measurements):
raise ValueError("output node can not be measured")

self._inputs = inputs
self._outputs = outputs
self._meas = measurements

@property
def inputs(self) -> list[int]:
"""Returns the inputs of the graph sorted by their position."""
return self._inputs
wlcsm marked this conversation as resolved.
Show resolved Hide resolved

@property
def outputs(self) -> list[int]:
"""Returns the outputs of the graph sorted by their position."""
return self._outputs

@property
def measurements(self) -> Mapping[int, Measurement]:
"""Returns a dictionary which maps each node to its measurement. Output
nodes are not measured and hence are not included in the dictionary."""
return self._meas

@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)
164 changes: 164 additions & 0 deletions graphix/pyzx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""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 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()
"""
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()))
wlcsm marked this conversation as resolved.
Show resolved Hide resolved
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()
wlcsm marked this conversation as resolved.
Show resolved Hide resolved
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 81 in graphix/pyzx.py

View check run for this annotation

Codecov / codecov/patch

graphix/pyzx.py#L80-L81

Added lines #L80 - L81 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.

wlcsm marked this conversation as resolved.
Show resolved Hide resolved
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 139 in graphix/pyzx.py

View check run for this annotation

Codecov / codecov/patch

graphix/pyzx.py#L138-L139

Added lines #L138 - L139 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