Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
59a4b49
:sparkles: Add ZXGraphState
Oct 5, 2025
d2172a9
:sparkles: Implement local_complement
Oct 5, 2025
fab5a8a
:sparkles: Implement pivot
Oct 5, 2025
5e47377
:sparkles: Add remove_clifford
Oct 5, 2025
6d02cdd
:sparkles: Implement remove_cliffords
Oct 5, 2025
0ce2db8
:sparkles: Implement convert_to_phase_gadget
Oct 5, 2025
b3a795e
:sparkles: Implement merge_yz_to_xy
Oct 5, 2025
c2ba8a5
:sparkles: Implement merge_yz_nodes
Oct 5, 2025
f89521b
:sparkles: Implement full_reduce
Oct 5, 2025
4525e5d
:art: Fix docstring
Oct 5, 2025
aeb1bf8
:art: Fix docstring
Oct 5, 2025
0b29a92
:art: Fix test
Oct 5, 2025
92843c0
:sparkles: Add a function to generate random MBQC circuit
Oct 5, 2025
3674192
:sparkles: Add demonstration for zxgraph simplification
Oct 5, 2025
d205a1d
:bug: Fix bug in generating circuit
Oct 5, 2025
d62f07b
:art: Fix type hint
Oct 5, 2025
ccc3e91
:art: Fix docstrings
Oct 5, 2025
c2a706b
:art: Fix type hint
Oct 5, 2025
0a8818b
:bulb: Add docs
Oct 5, 2025
bc70745
Merge branch 'master' into 102-add-zxgraphstate-class
Oct 17, 2025
06f0490
:bug: Fix bug after merge
Oct 17, 2025
14f76c5
:recycle: Refactor to_zx_graphstate
Oct 17, 2025
167ea75
:bug: Fix output node treatment
Oct 20, 2025
e44defb
:bug: Fix operation corresponding to lemma 4.7
Oct 20, 2025
6e97ac9
:bug: Fix handling of output node corresponding to lem. 4.7
Oct 20, 2025
c0a0649
:art: Improve readability
Oct 22, 2025
d882ed4
:recycle: Split case corresponds to lemma 4.11
Oct 22, 2025
26ba73e
:white_check_mark: Add tests for _is_noninput_with_io_nbrs
Oct 22, 2025
1fde3e9
:bug: Correct pivot definition
Oct 28, 2025
68dbbce
:sparkles: Add new case for removing cliffords
Oct 29, 2025
8207c0d
:fire: Delete unnecessary process
Nov 17, 2025
8b1eb91
:art: Fix docstring
Nov 17, 2025
47eb80d
:bug: Fix bug in preprocess
Nov 17, 2025
790aeb3
:bug: Fix expand_local_cliffords
Nov 17, 2025
92c962e
:art: Add demo to assert zxgraph simplification validity
Nov 17, 2025
f6a55e0
:art: Fix type hints
Nov 17, 2025
03a996a
Merge branch 'master' into 102-add-zxgraphstate-class
Nov 17, 2025
253d71f
:art: Fix import
Nov 17, 2025
525bd42
:bug: Fix bug in _expnad_input_local_cliffords
Nov 17, 2025
41f5fe8
:white_check_mark: Update tests for expand_local_cliffords
Nov 23, 2025
023d285
:art: Update expand_local_cliffords
Nov 23, 2025
be9a8e4
:art: Improve readability
Nov 24, 2025
6f37848
:art: Update docs for lc and pivot
Nov 24, 2025
9619ba4
:fire: Remove _swap
Nov 24, 2025
43bb480
:truck: Split gflow_wrapper
Nov 24, 2025
39af736
:art: Fix method name
Nov 24, 2025
adc9203
:white_check_mark: Add inner product test
Nov 24, 2025
35de92e
:art: Fix old test
Nov 24, 2025
c2f75b2
:art: Change update_lc_basis logic
Nov 25, 2025
204a61a
:bug: Fix bug
Nov 25, 2025
9ee7ee2
:art: Fix clifford expansion test
Nov 25, 2025
3a9b83a
:art: Fix zxgraphstate test
Nov 25, 2025
3e12fe8
:art: ruff
Nov 25, 2025
73fb584
:art: pyright
Nov 25, 2025
779d9bd
:art: ruff
Nov 25, 2025
4bfa792
:bug: Fix bug in input/output expansion
Nov 26, 2025
9f258e5
:white_check_mark: Add LocalClifford updating test in ZXGraphState layer
Nov 26, 2025
4b89957
:art: Fix import
Nov 26, 2025
65b7b7e
:sparkles: Add logically equivalent measurement basis map
Nov 27, 2025
2301bc7
:sparkles: Assure gflow existence
Nov 27, 2025
4f9ba16
:art: Fix tests
Nov 27, 2025
9fa0b72
:art: Fix
Nov 27, 2025
e6067c5
:art: Fix docs
Nov 27, 2025
2b88998
:art: Fix docstring
Nov 27, 2025
e1741f6
:bulb: Add doc settings
Nov 27, 2025
5bc194d
:art: Fix docs
Nov 27, 2025
8a31d96
:bug: Fix docs
Nov 27, 2025
69ffb37
:memo: Improve docs
Nov 27, 2025
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
17 changes: 17 additions & 0 deletions docs/source/gflow_utils.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Gflow Utils
===========

:mod:`graphqomb.gflow_utils` module
+++++++++++++++++++++++++++++++++++

.. automodule:: graphqomb.gflow_utils

Functions
---------

.. autofunction:: graphqomb.gflow_utils.gflow_wrapper

Constants
---------

.. autodata:: graphqomb.gflow_utils._EQUIV_MEAS_BASIS_MAP
2 changes: 2 additions & 0 deletions docs/source/references.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ Module reference
random_objects
feedforward
focus_flow
gflow_utils
command
pattern
pauli_frame
qompiler
scheduler
stim_compiler
visualizer
zxgraphstate
21 changes: 21 additions & 0 deletions docs/source/zxgraphstate.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
ZXGraphState
============

:mod:`graphqomb.zxgraphstate` module
+++++++++++++++++++++++++++++++++++++

.. automodule:: graphqomb.zxgraphstate

ZX Graph State
--------------

.. autoclass:: graphqomb.zxgraphstate.ZXGraphState
:members:
:show-inheritance:
:member-order: bysource

Functions
---------

.. autofunction:: graphqomb.zxgraphstate.to_zx_graphstate
.. autofunction:: graphqomb.zxgraphstate.complete_graph_edges
154 changes: 154 additions & 0 deletions examples/zxgraph_simplification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""
Basic example of simplifying a ZX-diagram.
==========================================

By using the full_reduce method,
we can remove all the internal Clifford nodes and some non-Clifford nodes from the graph state,
which generates a simpler ZX-diagram.
This example is a simple demonstration of the simplification process.

Note that as a result of the simplification, local Clifford operations are applied to the input/output nodes.
"""

# %%

from __future__ import annotations

from copy import deepcopy

import numpy as np

from graphqomb.gflow_utils import gflow_wrapper
from graphqomb.qompiler import qompile
from graphqomb.random_objects import generate_random_flow_graph
from graphqomb.simulator import PatternSimulator, SimulatorBackend
from graphqomb.visualizer import visualize
from graphqomb.zxgraphstate import ZXGraphState, to_zx_graphstate

FlowLike = dict[int, set[int]]

# %%
# Prepare an initial random graph state with flow
graph, flow = generate_random_flow_graph(width=3, depth=4, edge_p=0.5)
zx_graph, _ = to_zx_graphstate(graph)
visualize(zx_graph)

# %%
# We can compile the graph state into a measurement pattern, simulate it, and get the resulting statevector.
pattern = qompile(zx_graph, flow)
sim = PatternSimulator(pattern, backend=SimulatorBackend.StateVector)
sim.simulate()
statevec_original = sim.state


# %%
def print_boundary_lcs(zxgraph: ZXGraphState) -> None:
lc_map = zxgraph.local_cliffords
for node in zxgraph.input_node_indices | zxgraph.output_node_indices:
# check lc on input and output nodes
lc = lc_map.get(node, None)
if lc is not None:
if node in zxgraph.input_node_indices:
print(f"Input node {node} has local Clifford: alpha={lc.alpha}, beta={lc.beta}, gamma={lc.gamma}")
else:
print(f"Output node {node} has local Clifford: alpha={lc.alpha}, beta={lc.beta}, gamma={lc.gamma}")
else:
print(f"Node {node} has no local Clifford.")


def print_meas_bses(graph: ZXGraphState) -> None:
print("node | plane | angle (/pi)")
for node in graph.input_node_indices:
print(f"{node} (input)", graph.meas_bases[node].plane, graph.meas_bases[node].angle / np.pi)
for node in graph.physical_nodes - set(graph.input_node_indices) - set(graph.output_node_indices):
print(node, graph.meas_bases[node].plane, graph.meas_bases[node].angle / np.pi)
for node in graph.output_node_indices:
print(f"{node} (output)", "-", "-")


# %%
print_boundary_lcs(zx_graph)

# %%
# Initial graph state before simplification
print_meas_bses(zx_graph)


# %%
# Simplify the graph state by full_reduce method
zx_graph_smp = deepcopy(zx_graph)
zx_graph_smp.full_reduce()

# %%
# Simplified graph state after full_reduce.
visualize(zx_graph_smp)
print_meas_bses(zx_graph_smp)
print_boundary_lcs(zx_graph_smp)

# %%
# NOTE:
# At first glance, the input/output nodes appear to remain unaffected.
# However, note that a local Clifford operation is actually applied as a result of the action of the full_reduce method.

# If you visualize the graph state after executing the `expand_local_cliffords` method,
# you will see additional nodes connected to the former input/output nodes.


# %%
# Let us compare the graph state before and after simplification.
# We simulate the pattern obtained from the simplified graph state.
# Note that we need to call the `expand_local_cliffords` method before generating the pattern to get the gflow.

zx_graph_smp.expand_local_cliffords()
zx_graph_smp.to_xy() # to improve gflow search performance
zx_graph_smp.to_xz() # to improve gflow search performance
print("input_node_indices: ", set(zx_graph_smp.input_node_indices))
print("output_node_indices: ", set(zx_graph_smp.output_node_indices))
print("local_cliffords: ", zx_graph_smp.local_cliffords)

print_meas_bses(zx_graph_smp)
visualize(zx_graph_smp)
print_boundary_lcs(zx_graph_smp)

# %%
# Now we can obtain the gflow for the simplified graph state.
# Then, we compile the simplified graph state into a measurement pattern,
# simulate it, and get the resulting statevector.

# NOTE:
# gflow_wrapper does not support graph states with multiple subgraph structures in the gflow search wrapper below.
# Hence, in case you fail, ensure that the simplified graph state consists of a single connected component.
# To calculate the graph states with multiple subgraph structures,
# you need to calculate gflow for each connected component separately.
gflow_smp = gflow_wrapper(zx_graph_smp)
pattern_smp = qompile(zx_graph_smp, gflow_smp)
sim_smp = PatternSimulator(pattern_smp, backend=SimulatorBackend.StateVector)
sim_smp.simulate()

# %%
statevec_smp = sim_smp.state
# %%
# normalization check
print("norm of original statevector:", np.linalg.norm(statevec_original.state()))
print("norm of simplified statevector:", np.linalg.norm(statevec_smp.state()))

# %%
# Finally, we compare the expectation values of random observables before and after simplification.
rng = np.random.default_rng()
for i in range(len(zx_graph.input_node_indices)):
rand_mat = rng.random((2, 2)) + 1j * rng.random((2, 2))
rand_mat += rand_mat.T.conj()
exp = statevec_original.expectation(rand_mat, [i])
exp_cr = statevec_smp.expectation(rand_mat, [i])
print("Expectation values for rand_mat\n===============================")
print("rand_mat: \n", rand_mat)
print("Original: \t\t", exp)
print("After simplification: \t", exp_cr)

print("norm: ", np.linalg.norm(statevec_original.state()), np.linalg.norm(statevec_smp.state()))
print("data shape: ", statevec_original.state().shape, statevec_smp.state().shape)
psi_org = statevec_original.state()
psi_smp = statevec_smp.state()
print("inner product: ", np.abs(np.vdot(psi_org, psi_smp)))

# %%
42 changes: 42 additions & 0 deletions graphqomb/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,3 +397,45 @@ def meas_basis(plane: Plane, angle: float) -> NDArray[np.complex128]:
else:
typing_extensions.assert_never(plane)
return basis.astype(np.complex128)


def basis2tuple(meas_basis: MeasBasis) -> tuple[Plane, float]:
r"""Return the key (tuple[Plane, float]) for _EQUIV_MEAS_BASIS_MAP from a measurement basis.

Parameters
----------
meas_basis : `MeasBasis`
measurement basis

Returns
-------
tuple[Plane, float]
key for _EQUIV_MEAS_BASIS_MAP
"""
angle = round_clifford_angle(meas_basis.angle)
return (meas_basis.plane, angle)


def round_clifford_angle(angle: float, atol: float = 1e-9) -> float:
r"""Round the Clifford angle numerically to the nearest Clifford angle.

Parameters
----------
angle : `float`
angle in radians
atol : `float`, optional
absolute tolerance, by default 1e-9

Returns
-------
angle : `float`
For Clifford angles, the rounded angle.
If the angle is not a Clifford angle, return the original angle.
"""
clifford_angles = [0.0, np.pi / 2, np.pi, 3 * np.pi / 2]
for ca in clifford_angles:
if is_close_angle(angle, ca, atol):
angle = ca
break

return angle
2 changes: 1 addition & 1 deletion graphqomb/euler.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def update_lc_basis(lc: LocalClifford, basis: MeasBasis) -> PlannerMeasBasis:
`PlannerMeasBasis`
updated `PlannerMeasBasis`
"""
matrix = lc.matrix()
matrix = lc.matrix().conjugate().T
vector = basis.vector()

updated_vector = np.asarray(matrix @ vector, dtype=np.complex128)
Expand Down
111 changes: 111 additions & 0 deletions graphqomb/gflow_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Utilities for generalized flow (gflow) computation.

This module provides:

- `gflow_wrapper`: Thin adapter around ``swiflow.gflow`` so that gflow can be computed directly
from a `BaseGraphState` instance.
- `_EQUIV_MEAS_BASIS_MAP`: A mapping between equivalent measurement bases used to improve gflow finding performance.
"""

from __future__ import annotations

import math
from typing import TYPE_CHECKING

import networkx as nx
from swiflow import gflow
from swiflow.common import Plane as SfPlane

from graphqomb.common import Plane, PlannerMeasBasis

if TYPE_CHECKING:
from typing import Any

from networkx import Graph as NxGraph

from graphqomb.graphstate import BaseGraphState

FlowLike = dict[int, set[int]]


def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike:
"""Utilize ``swiflow.gflow`` to search gflow.

Parameters
----------
graphstate : `BaseGraphState`
graph state to find gflow

Returns
-------
``FlowLike``
gflow object

Raises
------
ValueError
If no gflow is found

Notes
-----
This wrapper does not support graph states with multiple subgraph structures.
"""
graph: NxGraph[Any] = nx.Graph()
graph.add_nodes_from(graphstate.physical_nodes)
graph.add_edges_from(graphstate.physical_edges)

bases = graphstate.meas_bases
planes = {node: bases[node].plane for node in bases}
swiflow_planes: dict[int, SfPlane] = {}
for node, plane in planes.items():
if plane == Plane.XY:
swiflow_planes[node] = SfPlane.XY
elif plane == Plane.YZ:
swiflow_planes[node] = SfPlane.YZ
elif plane == Plane.XZ:
swiflow_planes[node] = SfPlane.XZ
else:
msg = f"No match {plane}"
raise ValueError(msg)

gflow_object = gflow.find(
graph, set(graphstate.input_node_indices), set(graphstate.output_node_indices), swiflow_planes
)
if gflow_object is None:
msg = "No flow found"
raise ValueError(msg)

gflow_obj = gflow_object.f

return {node: {child for child in children if child != node} for node, children in gflow_obj.items()}


#: Mapping between equivalent measurement bases.
#:
#: This map is used to replace a measurement basis by an equivalent one
#: to improve gflow search performance.
#:
#: Key:
#: ``(Plane, angle)`` where angle is in radians.
#: Value:
#: :class:`~graphqomb.common.PlannerMeasBasis`.
_EQUIV_MEAS_BASIS_MAP: dict[tuple[Plane, float], PlannerMeasBasis] = {
# (XY, 0) <-> (XZ, pi/2)
(Plane.XY, 0.0): PlannerMeasBasis(Plane.XZ, 0.5 * math.pi),
(Plane.XZ, 0.5 * math.pi): PlannerMeasBasis(Plane.XY, 0.0),
# (XY, pi/2) <-> (YZ, pi/2)
(Plane.XY, 0.5 * math.pi): PlannerMeasBasis(Plane.YZ, 0.5 * math.pi),
(Plane.YZ, 0.5 * math.pi): PlannerMeasBasis(Plane.XY, 0.5 * math.pi),
# (XY, -pi/2) == (XY, 3pi/2) <-> (YZ, 3pi/2)
(Plane.XY, 1.5 * math.pi): PlannerMeasBasis(Plane.YZ, 1.5 * math.pi),
(Plane.YZ, 1.5 * math.pi): PlannerMeasBasis(Plane.XY, 1.5 * math.pi),
# (XY, pi) <-> (XZ, -pi/2) == (XZ, 3pi/2)
(Plane.XY, math.pi): PlannerMeasBasis(Plane.XZ, 1.5 * math.pi),
(Plane.XZ, 1.5 * math.pi): PlannerMeasBasis(Plane.XY, math.pi),
# (XZ, 0) <-> (YZ, 0)
(Plane.XZ, 0.0): PlannerMeasBasis(Plane.YZ, 0.0),
(Plane.YZ, 0.0): PlannerMeasBasis(Plane.XZ, 0.0),
# (XZ, pi) <-> (YZ, pi)
(Plane.XZ, math.pi): PlannerMeasBasis(Plane.YZ, math.pi),
(Plane.YZ, math.pi): PlannerMeasBasis(Plane.XZ, math.pi),
}
Loading