Skip to content

Commit

Permalink
Merge branch 'master' into sfx
Browse files Browse the repository at this point in the history
  • Loading branch information
y-richie-y authored Jul 16, 2024
2 parents 2f6c0ab + caa97c3 commit 9aaf12f
Show file tree
Hide file tree
Showing 28 changed files with 1,144 additions and 754 deletions.
32 changes: 25 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
The [ZX-calculus](http://zxcalculus.com) gives us a handy way to represent and work with quantum computations. ZXLive is an interactive tool for working with ZX. Draw graphs or load circuits and apply ZX rules. Intended for experimenting, building proofs, helping to write papers, showing off, or simply learning about ZX and quantum computing. It is powered by the [pyzx](https://github.com/Quantomatic/pyzx) open source library under the hood. The documentation is available at https://zxlive.readthedocs.io/
The [ZX-calculus](http://zxcalculus.com) gives us a handy way to represent and work with quantum computations. ZXLive is an interactive tool for working with ZX. Draw graphs or load circuits and apply ZX rules. Intended for experimenting, building proofs, helping to write papers, showing off, or simply learning about ZX and quantum computing. It is powered by the [pyzx](https://github.com/zxcalc/pyzx) open source library under the hood. The documentation is available at https://zxlive.readthedocs.io/

This project is in a pretty early stage, with lots more to come. Have a look at the [Issue Tracker](https://github.com/Quantomatic/zxlive/issues) to see what's in the pipeline.
This project is in a pretty early stage, with lots more to come. Have a look at the [Issue Tracker](https://github.com/zxcalc/zxlive/issues) to see what's in the pipeline.


## Instructions

To install from source, you need Python >= 3.9 and pip. If you have those, just run:

git clone https://github.com/Quantomatic/zxlive.git
git clone https://github.com/zxcalc/zxlive.git
cd zxlive
pip install .

Then, you can run ZXLive by typing `python3 -m zxlive`.
Then, you can run ZXLive by typing `zxlive`.

Note that ZXLive requires the most recent development version of PyZX. If you already have a version of PyZX installed, first uninstall it with `pip uninstall pyzx`, and then run `pip install .` in the ZXLive folder (or manually install the latest PyZX version using `pip install @git+https://github.com/zxcalc/pyzx.git`).

# Testing

To ensure the quality and correctness of this project, several testing and linting tools are utilized.
Below are the steps to run the tests and checks:

## 1. Static Type Checks with mypy

You can run mypy to perform static type checking:

mypy zxlive

## 2. Unit Tests with pytest

Execute the test suite using pytest to ensure all tests pass:

pytest

# Documentation

This project uses Sphinx to build a ReadTheDocs page. In addition, it uses the MyST preprocessor to allow us to write documentation in a Markdown-like format rather than Sphinx's native rich structured text format.

The documentation can be built with

```
cd doc; make html
```
cd doc
make html
11 changes: 5 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ classifiers = [
"Topic :: Scientific/Engineering"
]
dependencies = [
"PySide6",
"pyzx @ git+https://github.com/Quantomatic/pyzx.git",
"PySide6 >= 6.7.2",
"pyzx @ git+https://github.com/zxcalc/pyzx.git",
"networkx",
"numpy",
"shapely",
Expand All @@ -38,7 +38,6 @@ dependencies = [

[project.optional-dependencies]
test = [
"PySide6-stubs",
"mypy",
"pyproject-flake8",
"pylint",
Expand All @@ -53,9 +52,9 @@ doc = [
]

[project.urls]
Homepage = "https://github.com/Quantomatic/zxlive"
Repository = "https://github.com/Quantomatic/zxlive"
Issue-Tracker = "https://github.com/Quantomatic/zxlive/issues"
Homepage = "https://github.com/zxcalc/zxlive"
Repository = "https://github.com/zxcalc/zxlive"
Issue-Tracker = "https://github.com/zxcalc/zxlive/issues"

[tool.setuptools]
packages = [
Expand Down
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ pyzx>=0.8.0
networkx~=3.1
numpy~=1.25.2
pytest-qt~=4.2.0
PySide6~=6.5.2
PySide6-stubs
PySide6~=6.7.2,
shapely~=2.0.1
shapely-stubs @ git+https://github.com/ciscorn/shapely-stubs.git
pyperclip>=1.8.1
Expand Down
2 changes: 1 addition & 1 deletion test/test_mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def check_file_format(filename: str) -> None:
def test_proof_cleanup_before_close(app: MainWindow, qtbot: QtBot) -> None:
# Regression test to check that the app doesn't crash when closing a proof tab with a derivation in progress,
# due to accessing the graph after it has been deallocated.
# See https://github.com/Quantomatic/zxlive/issues/218 for context.
# See https://github.com/zxcalc/zxlive/issues/218 for context.
assert app.active_panel is not None
assert isinstance(app.active_panel, GraphEditPanel)
qtbot.mouseClick(app.active_panel.start_derivation, QtCore.Qt.MouseButton.LeftButton)
Expand Down
2 changes: 1 addition & 1 deletion zxlive/animations.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ def unfuse(before: GraphT, after: GraphT, src: VT, scene: GraphScene) -> QAbstra
duration=700, ease=QEasingCurve(QEasingCurve.Type.OutElastic))


def make_animation(self: RewriteAction, panel: ProofPanel, g, matches, rem_verts) -> tuple:
def make_animation(self: RewriteAction, panel: ProofPanel, g: GraphT, matches: list, rem_verts: list[VT]) -> tuple:
anim_before = None
anim_after = None
if self.name == operations['spider']['text'] or self.name == operations['fuse_w']['text']:
Expand Down
4 changes: 3 additions & 1 deletion zxlive/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@

from .mainwindow import MainWindow
from .common import get_data, GraphT
from .settings import display_setting
from typing import Optional, cast

# The following hack is needed on windows in order to show the icon in the taskbar
# See https://stackoverflow.com/questions/1551605/how-to-set-applications-taskbar-icon-in-windows-7/1552105#1552105
import os
if os.name == 'nt':
import ctypes
myappid = 'quantomatic.zxlive.zxlive.1.0.0' # arbitrary string
myappid = 'zxcalc.zxlive.zxlive.1.0.0' # arbitrary string
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) # type: ignore


Expand All @@ -45,6 +46,7 @@ class ZXLive(QApplication):

def __init__(self) -> None:
super().__init__(sys.argv)
self.setFont(display_setting.font)
self.setApplicationName('ZXLive')
self.setDesktopFileName('ZXLive')
self.setApplicationVersion('0.2.0') # TODO: read this from pyproject.toml if possible
Expand Down
14 changes: 9 additions & 5 deletions zxlive/base_panel.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Iterator, Optional, Sequence
from typing import Iterator, Optional, Sequence, Type

from PySide6.QtCore import Signal
from PySide6.QtGui import QAction
from PySide6.QtWidgets import (QAbstractButton, QButtonGroup, QSplitter,
QToolBar, QVBoxLayout, QWidget)

from .eitem import EItem
from .animations import AnimatedUndoStack
from .commands import ChangeEdgeCurve, SetGraph
from .common import GraphT, new_graph
Expand All @@ -34,7 +35,7 @@ def __init__(self, *args: QWidget | QAction, exclusive: bool = False) -> None:
class BasePanel(QWidget):
"""Base class implementing functionality shared between the edit and
proof panels."""
splitter_sizes = dict()
splitter_sizes: dict[Type[BasePanel], list[int]] = dict()

graph_scene: GraphScene
graph_view: GraphView
Expand Down Expand Up @@ -112,12 +113,15 @@ def copy_selection(self) -> GraphT:
def update_colors(self) -> None:
self.graph_scene.update_colors()

def sync_splitter_sizes(self):
def sync_splitter_sizes(self) -> None:
self.splitter_sizes[self.__class__] = self.splitter.sizes()

def set_splitter_size(self):
def set_splitter_size(self) -> None:
if self.__class__ in self.splitter_sizes:
self.splitter.setSizes(self.splitter_sizes[self.__class__])

def change_edge_curves(self, eitem, new_distance, old_distance):
def change_edge_curves(self, eitem: EItem, new_distance: float, old_distance: float) -> None:
self.undo_stack.push(ChangeEdgeCurve(self.graph_view, eitem, new_distance, old_distance))

def update_font(self) -> None:
self.graph_view.update_font()
90 changes: 50 additions & 40 deletions zxlive/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
from pyzx.symbolic import Poly
from pyzx.utils import EdgeType, VertexType, get_w_partner, vertex_is_w, get_w_io, get_z_box_label, set_z_box_label

from .common import ET, VT, W_INPUT_OFFSET, GraphT, setting
from .common import ET, VT, W_INPUT_OFFSET, GraphT
from .settings import display_setting
from .eitem import EItem
from .graphview import GraphView
from .proof import ProofModel, Rewrite
from .proof import ProofModel, ProofStepView, Rewrite


@dataclass
Expand Down Expand Up @@ -101,11 +102,11 @@ def redo(self) -> None:
class ChangeNodeType(BaseCommand):
"""Changes the color of a set of spiders."""
vs: list[VT] | set[VT]
vty: VertexType.Type
vty: VertexType

WInfo = namedtuple('WInfo', ['partner', 'partner_type', 'partner_pos', 'neighbors'])

_old_vtys: Optional[list[VertexType.Type]] = field(default=None, init=False)
_old_vtys: Optional[list[VertexType]] = field(default=None, init=False)
_old_w_info: Optional[dict[VT, WInfo]] = field(default=None, init=False)
_new_w_inputs: Optional[list[VT]] = field(default=None, init=False)

Expand Down Expand Up @@ -170,7 +171,7 @@ def redo(self) -> None:
class ChangeEdgeColor(BaseCommand):
"""Changes the color of a set of edges"""
es: Iterable[ET]
ety: EdgeType.Type
ety: EdgeType

_old_etys: Optional[list[EdgeType]] = field(default=None, init=False)

Expand All @@ -192,7 +193,7 @@ class AddNode(BaseCommand):
"""Adds a new spider at a given position."""
x: float
y: float
vty: VertexType.Type
vty: VertexType

_added_vert: Optional[VT] = field(default=None, init=False)

Expand All @@ -202,8 +203,8 @@ def undo(self) -> None:
self.update_graph_view()

def redo(self) -> None:
y = round(self.y * setting.SNAP_DIVISION) / setting.SNAP_DIVISION
x = round(self.x * setting.SNAP_DIVISION) / setting.SNAP_DIVISION
y = round(self.y * display_setting.SNAP_DIVISION) / display_setting.SNAP_DIVISION
x = round(self.x * display_setting.SNAP_DIVISION) / display_setting.SNAP_DIVISION
self._added_vert = self.g.add_vertex(self.vty, y,x)
self.update_graph_view()

Expand All @@ -224,8 +225,8 @@ def undo(self) -> None:
self.update_graph_view()

def redo(self) -> None:
y = round(self.y * setting.SNAP_DIVISION) / setting.SNAP_DIVISION
x = round(self.x * setting.SNAP_DIVISION) / setting.SNAP_DIVISION
y = round(self.y * display_setting.SNAP_DIVISION) / display_setting.SNAP_DIVISION
x = round(self.x * display_setting.SNAP_DIVISION) / display_setting.SNAP_DIVISION
self._added_input_vert = self.g.add_vertex(VertexType.W_INPUT, y - W_INPUT_OFFSET, self.x)
self._added_output_vert = self.g.add_vertex(VertexType.W_OUTPUT, y, x)
self.g.add_edge((self._added_input_vert, self._added_output_vert), EdgeType.W_IO)
Expand All @@ -236,7 +237,7 @@ class AddEdge(BaseCommand):
"""Adds an edge between two spiders."""
u: VT
v: VT
ety: EdgeType.Type
ety: EdgeType

def undo(self) -> None:
self.g.remove_edge((self.u, self.v, self.ety))
Expand Down Expand Up @@ -269,6 +270,25 @@ def redo(self) -> None:
self.g.set_qubit(v, y)
self.update_graph_view()

@dataclass
class MoveNodeProofMode(MoveNode):
step_view: ProofStepView

def __init__(self, graph_view: GraphView, vs: list[tuple[VT, float, float]], step_view: ProofStepView) -> None:
super().__init__(graph_view, vs)
self.step_view = step_view
self.proof_step_index = int(step_view.currentIndex().row())

def undo(self) -> None:
self.step_view.move_to_step(self.proof_step_index)
super().undo()
self.step_view.model().set_graph(self.proof_step_index, self.graph_view.graph_scene.g)

def redo(self) -> None:
self.step_view.move_to_step(self.proof_step_index)
super().redo()
self.step_view.model().set_graph(self.proof_step_index, self.graph_view.graph_scene.g)


@dataclass
class ChangeEdgeCurve(BaseCommand):
Expand All @@ -291,7 +311,7 @@ class AddIdentity(BaseCommand):
"""Adds an X or Z identity spider on an edge between two vertices."""
u: VT
v: VT
vty: VertexType.Type
vty: VertexType

_new_vert: Optional[VT] = field(default=None, init=False)

Expand Down Expand Up @@ -417,38 +437,28 @@ def undo(self) -> None:


@dataclass
class GoToRewriteStep(SetGraph):
"""Shows the graph at some step in the proof.
Undoing returns to the previously selected proof step.
"""
class GroupRewriteSteps(BaseCommand):
step_view: ProofStepView
start_index: int
end_index: int

def __init__(self, graph_view: GraphView, step_view: QListView, old_step: int, step: int) -> None:
proof_model = step_view.model()
assert isinstance(proof_model, ProofModel)
def redo(self) -> None:
self.step_view.model().group_steps(self.start_index, self.end_index)
self.step_view.move_to_step(self.start_index + 1)

# Save any vertex rearrangements to the proof step
proof_model.set_graph(old_step, graph_view.graph_scene.g)
def undo(self) -> None:
self.step_view.model().ungroup_steps(self.start_index)
self.step_view.move_to_step(self.end_index + 1)

SetGraph.__init__(self, graph_view, proof_model.get_graph(step))
self.step_view = step_view
self.step = step
self.old_step = old_step
@dataclass
class UngroupRewriteSteps(BaseCommand):
step_view: ProofStepView
group_index: int

def redo(self) -> None:
idx = self.step_view.model().index(self.step, 0, QModelIndex())
self.step_view.clearSelection()
self.step_view.selectionModel().blockSignals(True)
self.step_view.setCurrentIndex(idx)
self.step_view.selectionModel().blockSignals(False)
self.step_view.update(idx)
super().redo()
self.step_view.model().ungroup_steps(self.group_index)
self.step_view.move_to_step(self.group_index + 1)

def undo(self) -> None:
idx = self.step_view.model().index(self.old_step, 0, QModelIndex())
self.step_view.clearSelection()
self.step_view.selectionModel().blockSignals(True)
self.step_view.setCurrentIndex(idx)
self.step_view.selectionModel().blockSignals(False)
self.step_view.update(idx)
super().undo()
self.step_view.model().group_steps(self.group_index, self.group_index + 1)
self.step_view.move_to_step(self.group_index + 1)
Loading

0 comments on commit 9aaf12f

Please sign in to comment.