Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
93cb042
draw graph example
masa10-f Aug 26, 2025
ada1ba0
implement visualizer
masa10-f Aug 26, 2025
1c72c51
config for sphinx doc
masa10-f Aug 26, 2025
207eddd
set up for sphinx doc
masa10-f Aug 26, 2025
94d3318
add matplotlib and networkx as requiremenets
masa10-f Aug 26, 2025
90e7da2
ignore types in matplotlib
masa10-f Aug 26, 2025
7052e02
add demo of different meas plane
masa10-f Aug 26, 2025
d62d7ba
enrich visualizer
masa10-f Aug 26, 2025
510878a
add multiple demo
masa10-f Aug 26, 2025
4abc52e
tune font size
masa10-f Aug 26, 2025
5f44dc4
ignore png
masa10-f Aug 26, 2025
a8ede6b
update color map
masa10-f Sep 2, 2025
5f5aa02
add legend
masa10-f Sep 2, 2025
b879130
change display style of Pauli node and modify size calculation logic
masa10-f Sep 2, 2025
a724cb0
adjust node size
masa10-f Sep 2, 2025
98d8817
update demo
masa10-f Sep 2, 2025
d39545a
fix ruff and pyright error
masa10-f Sep 2, 2025
c5fa8b5
remove scale
masa10-f Sep 2, 2025
e370f97
locate the legend at the buttom
masa10-f Sep 2, 2025
24fb754
use AxisMeasBasis
masa10-f Sep 5, 2025
12492f2
move is_clifford_angle and is_close_angle
masa10-f Sep 5, 2025
cf2f11b
update unittest
masa10-f Sep 5, 2025
f16aeb4
complete the implementation of visualizer
masa10-f Sep 5, 2025
b8d1d7c
update docs
masa10-f Sep 5, 2025
5f42a29
ruff
masa10-f Sep 5, 2025
7dac4c5
remove float cast
masa10-f Sep 8, 2025
4dbd58e
use tuple
masa10-f Sep 8, 2025
a23af3a
graphix_zx/visualizer.py
masa10-f Sep 8, 2025
1239d31
Revert "graphix_zx/visualizer.py"
masa10-f Sep 8, 2025
889bbab
implement compatibility layer for ColorMap
masa10-f Sep 8, 2025
e1ad8f0
delete unnecessary line
masa10-f Sep 8, 2025
1bb84fb
remove .show() call
masa10-f Sep 29, 2025
a5fd246
add ax as an input variable and return fig and ax
masa10-f Sep 29, 2025
aaf8620
use fig/ax style and remove plt.xxx
masa10-f Sep 29, 2025
1c27993
remove figure
masa10-f Sep 29, 2025
881ec8b
rename get_xxx
masa10-f Sep 29, 2025
2f1a01e
use NamedTuple for complex setup func
masa10-f Sep 29, 2025
bb6ea80
use unpack
masa10-f Sep 29, 2025
b8fa9d9
delete pyright pragmas
masa10-f Sep 29, 2025
89eaf52
remove unnecessary float cast
masa10-f Sep 29, 2025
c9dc687
ruff
masa10-f Sep 29, 2025
a224191
completely use fig/ax style
masa10-f Sep 29, 2025
1aae3d9
update example
masa10-f Sep 29, 2025
d56472e
add ref to matplotlib
masa10-f Sep 29, 2025
1573686
add docstring
masa10-f Sep 29, 2025
e113ca2
tune figure aspect ratio
masa10-f Sep 29, 2025
6a79869
rename get_pauli_axis -> determine_pauli_axis
masa10-f Sep 29, 2025
17a13a9
fix doc
masa10-f Sep 29, 2025
068e153
unify into math.pi
masa10-f Oct 2, 2025
f56b44e
type cast
masa10-f Oct 2, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,4 @@ cython_debug/
# Custom stuff
docs/source/gallery
docs/source/sg_execution_times.rst
*.png
6 changes: 6 additions & 0 deletions docs/source/common.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ Measurement Basis Classes
Functions
---------

.. autofunction:: graphix_zx.common.is_close_angle

.. autofunction:: graphix_zx.common.is_clifford_angle

.. autofunction:: graphix_zx.common.determine_pauli_axis

.. autofunction:: graphix_zx.common.default_meas_basis

.. autofunction:: graphix_zx.common.meas_basis
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"numpy": ("https://numpy.org/doc/stable/", None),
"matplotlib": ("https://matplotlib.org/stable/", None),
}

html_context = {
Expand Down
4 changes: 0 additions & 4 deletions docs/source/euler.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ Functions

.. autofunction:: graphix_zx.euler.bloch_sphere_coordinates

.. autofunction:: graphix_zx.euler.is_close_angle

.. autofunction:: graphix_zx.euler.is_clifford_angle

.. autofunction:: graphix_zx.euler.meas_basis_info

.. autofunction:: graphix_zx.euler.update_lc_lc
Expand Down
1 change: 1 addition & 0 deletions docs/source/references.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ Module reference
pauli_frame
qompiler
scheduler
visualizer
5 changes: 5 additions & 0 deletions docs/source/visualizer.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Visualizer Module
=================

.. automodule:: graphix_zx.visualizer
:members:
146 changes: 146 additions & 0 deletions examples/draw_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""
Graph Visualizer
================

Simple example to draw a GraphState in graphix-zx.
"""

# %%
import matplotlib.pyplot as plt
import numpy as np

from graphix_zx.common import Axis, AxisMeasBasis, Plane, PlannerMeasBasis, Sign
from graphix_zx.graphstate import GraphState
from graphix_zx.random_objects import generate_random_flow_graph
from graphix_zx.visualizer import visualize

# Create a random flow graph
random_graph, flow = generate_random_flow_graph(5, 5)


# Visualize the flow graph
ax = visualize(random_graph)
plt.show()
print("Displayed flow graph")

# %%
# Create a demo graph with different measurement planes and input/output nodes

demo_graph = GraphState()

# Add input nodes
input_node1 = demo_graph.add_physical_node()
input_node2 = demo_graph.add_physical_node()
q_index1 = demo_graph.register_input(input_node1)
q_index2 = demo_graph.register_input(input_node2)

# Set measurement bases for input nodes (XY plane with different angles)
demo_graph.assign_meas_basis(input_node1, AxisMeasBasis(Axis.X, Sign.PLUS))
demo_graph.assign_meas_basis(input_node2, PlannerMeasBasis(Plane.XY, np.pi / 6))

# Add internal nodes with different measurement planes
internal_node1 = demo_graph.add_physical_node()
internal_node2 = demo_graph.add_physical_node()
internal_node3 = demo_graph.add_physical_node()

# Set measurement bases for internal nodes
# XZ plane (blue) with angle π/4
demo_graph.assign_meas_basis(internal_node1, PlannerMeasBasis(Plane.XZ, np.pi / 4))
# YZ plane (red) with angle π/3
demo_graph.assign_meas_basis(internal_node2, PlannerMeasBasis(Plane.YZ, np.pi / 3))
# XZ plane (blue) with angle π/2
demo_graph.assign_meas_basis(internal_node3, PlannerMeasBasis(Plane.XZ, np.pi / 2))

# Add output nodes
output_node1 = demo_graph.add_physical_node()
output_node2 = demo_graph.add_physical_node()
demo_graph.register_output(output_node1, 0)
demo_graph.register_output(output_node2, 1)

# Create edges to connect the graph
demo_graph.add_physical_edge(input_node1, internal_node1)
demo_graph.add_physical_edge(input_node2, internal_node2)
demo_graph.add_physical_edge(internal_node1, internal_node3)
demo_graph.add_physical_edge(internal_node2, internal_node3)
demo_graph.add_physical_edge(internal_node3, output_node1)
demo_graph.add_physical_edge(internal_node1, output_node2)

print("Demo graph with XZ and YZ measurement planes:")
print(f"Input nodes: {list(demo_graph.input_node_indices.keys())}")
print(f"Output nodes: {list(demo_graph.output_node_indices.keys())}")
print(f"All physical nodes: {demo_graph.physical_nodes}")
print("Internal nodes with measurement bases:")
for node, basis in demo_graph.meas_bases.items():
print(f" Node {node}: {basis.plane.name} plane, angle={basis.angle:.3f}")

# Visualize the demo graph with labels
ax = visualize(demo_graph, show_node_labels=True)
plt.show()
print("Displayed demo graph with labels")

# Visualize without labels to see just the colored patterns
print("\n--- Same graph without node labels ---")
ax = visualize(demo_graph, show_node_labels=False)
plt.show()
print("Displayed demo graph without labels")

# %%
# Create another demo graph with Pauli measurements (θ=0, π)
pauli_demo_graph = GraphState()

# Add nodes for Pauli measurements
pauli_input = pauli_demo_graph.add_physical_node()
pauli_demo_graph.register_input(pauli_input)

# Create internal nodes with Pauli measurements
x_measurement_node = pauli_demo_graph.add_physical_node() # X measurement: XY plane, θ=0
y_measurement_node = pauli_demo_graph.add_physical_node() # Y measurement: YZ plane, θ=π/2
z_measurement_node = pauli_demo_graph.add_physical_node() # Z measurement: XZ plane, θ=π

# Set Pauli measurement bases
pauli_demo_graph.assign_meas_basis(pauli_input, AxisMeasBasis(Axis.X, Sign.PLUS)) # X+
pauli_demo_graph.assign_meas_basis(x_measurement_node, AxisMeasBasis(Axis.X, Sign.PLUS)) # X+
pauli_demo_graph.assign_meas_basis(y_measurement_node, AxisMeasBasis(Axis.Y, Sign.PLUS)) # Y+
pauli_demo_graph.assign_meas_basis(z_measurement_node, AxisMeasBasis(Axis.Z, Sign.MINUS)) # Z-

# Add output node
pauli_output = pauli_demo_graph.add_physical_node()
pauli_demo_graph.register_output(pauli_output, 0)

# Connect nodes
pauli_demo_graph.add_physical_edge(pauli_input, x_measurement_node)
pauli_demo_graph.add_physical_edge(x_measurement_node, y_measurement_node)
pauli_demo_graph.add_physical_edge(y_measurement_node, z_measurement_node)
pauli_demo_graph.add_physical_edge(z_measurement_node, pauli_output)

print("\\nPauli measurement demo graph:")
print(f"Input nodes: {list(pauli_demo_graph.input_node_indices.keys())}")
print(f"Output nodes: {list(pauli_demo_graph.output_node_indices.keys())}")
print("Pauli measurement nodes (will show bordered patterns):")
print(" - X measurement (θ=0°): Green center + Blue border (XY+XZ planes)")
print(" - Y measurement (θ=90°): Red center + Green border (YZ+XY planes)")
print(" - Z measurement (θ=180°): Blue center + Red border (XZ+YZ planes)")
print("Individual nodes:")
for node, basis in pauli_demo_graph.meas_bases.items():
plane_name = basis.plane.name
angle_deg = basis.angle * 180 / np.pi
print(f" Node {node}: {plane_name} plane, angle={basis.angle:.3f} ({angle_deg:.1f}°)")

# Visualize the Pauli demo graph (using bordered-node visualization)
ax = visualize(pauli_demo_graph, show_node_labels=True)
plt.show()
print("Displayed Pauli demo graph")

# Demo with larger nodes and no labels
print("\n--- Larger nodes without labels ---")
ax = visualize(pauli_demo_graph, show_node_labels=False)
plt.show()
print("Displayed Pauli demo graph without labels")

# Demo without legend to avoid overlap
print("\n--- Without legend to avoid overlap ---")
ax = visualize(pauli_demo_graph, show_node_labels=True, show_legend=False)
plt.show()
print("Displayed Pauli demo graph without legend")

# %%
88 changes: 85 additions & 3 deletions graphix_zx/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
- `MeasBasis`: Abstract class to represent a measurement basis.
- `PlannerMeasBasis`: Class to represent a planner measurement basis.
- `AxisMeasBasis`: Class to represent an axis measurement basis.
- `is_close_angle`: Check if an angle is close to a target angle.
- `is_clifford_angle`: Check if an angle is a Clifford angle.
- `determine_pauli_axis`: Function to determine Pauli axis for a measurement basis.
- `default_meas_basis`: Function to return the default measurement basis.
- `meas_basis`: Function to get the measurement basis vector.
"""
Expand Down Expand Up @@ -126,7 +129,7 @@ def flip(self) -> PlannerMeasBasis:
`PlannerMeasBasis`
flipped PlannerMeasBasis
"""
return PlannerMeasBasis(self.plane, self.angle + np.pi)
return PlannerMeasBasis(self.plane, self.angle + math.pi)

@typing_extensions.override
def conjugate(self) -> PlannerMeasBasis:
Expand Down Expand Up @@ -229,9 +232,9 @@ def angle(self) -> float:
msg = "The axis must be one of X, Y, Z"
raise TypeError(msg)
if self.axis == Axis.Y:
angle = np.pi / 2 if self.sign == Sign.PLUS else 3 * np.pi / 2
angle = math.pi / 2 if self.sign == Sign.PLUS else 3 * math.pi / 2
else:
angle = 0 if self.sign == Sign.PLUS else np.pi
angle = 0 if self.sign == Sign.PLUS else math.pi
return angle

@typing_extensions.override
Expand Down Expand Up @@ -270,6 +273,85 @@ def vector(self) -> NDArray[np.complex128]:
return meas_basis(self.plane, self.angle)


def is_close_angle(angle: float, target: float, atol: float = 1e-9) -> bool:
"""Check if an angle is close to a target angle.

Parameters
----------
angle : `float`
angle to check
target : `float`
target angle
atol : `float`, optional
absolute tolerance, by default 1e-9

Returns
-------
`bool`
`True` if the angle is close to the target angle
"""
diff_angle = (angle - target) % (2 * math.pi)

if diff_angle > math.pi:
diff_angle = 2 * math.pi - diff_angle
return bool(np.isclose(diff_angle, 0, atol=atol))


def is_clifford_angle(angle: float, atol: float = 1e-9) -> bool:
"""Check if an angle is a Clifford angle.

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

Returns
-------
`bool`
`True` if the angle is a Clifford angle
"""
angle_preprocessed = angle % (2 * math.pi)
return any(
is_close_angle(angle_preprocessed, target, atol=atol) for target in (0.0, math.pi / 2, math.pi, 3 * math.pi / 2)
)


def determine_pauli_axis(meas_bases: MeasBasis) -> Axis | None:
"""Determine Pauli axis for a measurement basis if it's a Pauli measurement.

Parameters
----------
meas_bases : `MeasBasis`
Measurement basis to check

Returns
-------
`Axis` | None
Pauli axis if this is a Pauli measurement, None otherwise
"""
angle = meas_bases.angle

if not is_clifford_angle(angle):
return None

if is_close_angle(angle, 0) or is_close_angle(angle, math.pi):
# X measurement (XY plane, θ=0 or π) or Z measurement (XZ plane, θ=0 or π)
if meas_bases.plane == Plane.XY:
return Axis.X
if meas_bases.plane in {Plane.XZ, Plane.YZ}:
return Axis.Z
elif is_close_angle(angle, math.pi / 2) or is_close_angle(angle, 3 * math.pi / 2):
# Y measurement can occur in XY plane (θ=π/2 or 3π/2) or YZ plane (θ=π/2 or 3π/2)
if meas_bases.plane in {Plane.XY, Plane.YZ}:
return Axis.Y
if meas_bases.plane == Plane.XZ:
return Axis.X

return None


def default_meas_basis() -> PlannerMeasBasis:
"""Return the default measurement basis.

Expand Down
Loading