Skip to content

Commit

Permalink
Merge pull request #1 from Quantomatic/master
Browse files Browse the repository at this point in the history
Merge master to current
  • Loading branch information
RazinShaikh authored Nov 15, 2023
2 parents 0f634c8 + bb898d6 commit 2bb5f87
Show file tree
Hide file tree
Showing 29 changed files with 1,126 additions and 860 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ dist
*.qasm
.env
*.zxr
.ipynb_checkpoints
77 changes: 77 additions & 0 deletions embedded_zxlive_demo.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "dc0b153f-dc2f-447e-82de-6e71b9d389d2",
"metadata": {},
"source": [
"# Demo of embedded ZXLive running inside Jupyter Notebook\n",
"\n",
"First, run the cell below. An instance of ZXLive will open, with two identical graphs."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0e45e8da-f33b-4d13-8327-e394ae096ed9",
"metadata": {},
"outputs": [],
"source": [
"%gui qt6\n",
"from zxlive import app\n",
"\n",
"import pyzx as zx\n",
"\n",
"g = zx.Graph()\n",
"g.add_vertex(zx.VertexType.Z, 0, 0)\n",
"g.add_vertex(zx.VertexType.X, 0, 1)\n",
"g.add_edge((0, 1))\n",
"zx.draw(g)\n",
"\n",
"zxl = app.get_embedded_app()\n",
"zxl.edit_graph(g, 'g1')\n",
"zxl.edit_graph(g, 'g2')"
]
},
{
"cell_type": "markdown",
"id": "88f425a0-d50d-40e0-86cf-eecbc3a27277",
"metadata": {},
"source": [
"After making some edits and saving them from within ZXLive, run the following cell to see the changes."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0a0a8a17-1795-4b13-aedf-30b346388e23",
"metadata": {},
"outputs": [],
"source": [
"zx.draw(zxl.get_copy_of_graph('g1'))\n",
"zx.draw(zxl.get_copy_of_graph('g2'))"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ dependencies = [
"networkx",
"numpy",
"shapely",
"lark>=1.1.5",
"pyperclip>=1.8.1"
"pyperclip"
]

[project.optional-dependencies]
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
pyzx @ git+https://github.com/Quantomatic/pyzx
lark~=1.1.7
networkx~=3.1
numpy~=1.25.2
pytest-qt~=4.2.0
Expand Down
53 changes: 1 addition & 52 deletions test/test_editor_base_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,60 +18,9 @@

import pytest

from zxlive.editor_base_panel import string_to_fraction, string_to_complex
from zxlive.poly import Poly, Term, Var, new_var
from zxlive.editor_base_panel import string_to_complex


def test_string_to_fraction() -> None:
types_dict = {'a': False, 'b': False}

def _new_var(name: str) -> Poly:
return new_var(name, types_dict)

# Test empty input clears the phase.
assert string_to_fraction('', _new_var) == Fraction(0)

# Test different ways of specifying integer multiples of pi.
assert string_to_fraction('3', _new_var) == Fraction(3)
assert string_to_fraction('3pi', _new_var) == Fraction(3)
assert string_to_fraction('3*pi', _new_var) == Fraction(3)
assert string_to_fraction('pi*3', _new_var) == Fraction(3)

# Test different ways of specifying fractions.
assert string_to_fraction('pi/2', _new_var) == Fraction(1, 2)
assert string_to_fraction('-pi/2', _new_var) == Fraction(-1, 2)
assert string_to_fraction('5/2', _new_var) == Fraction(5, 2)
assert string_to_fraction('5pi/2', _new_var) == Fraction(5, 2)
assert string_to_fraction('5*pi/2', _new_var) == Fraction(5, 2)
assert string_to_fraction('pi*5/2', _new_var) == Fraction(5, 2)
assert string_to_fraction('5/2pi', _new_var) == Fraction(5, 2)
assert string_to_fraction('5/2*pi', _new_var) == Fraction(5, 2)
assert string_to_fraction('5/pi*2', _new_var) == Fraction(5, 2)

# Test different ways of specifying floats.
assert string_to_fraction('5.5', _new_var) == Fraction(11, 2)
assert string_to_fraction('5.5pi', _new_var) == Fraction(11, 2)
assert string_to_fraction('25e-1', _new_var) == Fraction(5, 2)
assert string_to_fraction('5.5*pi', _new_var) == Fraction(11, 2)
assert string_to_fraction('pi*5.5', _new_var) == Fraction(11, 2)

# Test a fractional phase specified with variables.
assert (string_to_fraction('a*b', _new_var) ==
Poly([(1, Term([(Var('a', types_dict), 1), (Var('b', types_dict), 1)]))]))
assert (string_to_fraction('2*a', _new_var) ==
Poly([(2, Term([(Var('a', types_dict), 1)]))]))
assert (string_to_fraction('2a', _new_var) ==
Poly([(2, Term([(Var('a', types_dict), 1)]))]))
assert (string_to_fraction('3/2a', _new_var) ==
Poly([(3/2, Term([(Var('a', types_dict), 1)]))]))
assert (string_to_fraction('3a+2b', _new_var) ==
Poly([(3, Term([(Var('a', types_dict), 1)])), (2, Term([(Var('b', types_dict), 1)]))]))


# Test bad input.
with pytest.raises(ValueError):
string_to_fraction('bad input', _new_var)

def test_string_to_complex() -> None:
# Test empty input clears the phase.
assert string_to_complex('') == 0
Expand Down
67 changes: 64 additions & 3 deletions zxlive/animations.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
from __future__ import annotations

import itertools
import random
from typing import Optional, Callable
from typing import Optional, Callable, TYPE_CHECKING

from PySide6.QtCore import QEasingCurve, QPointF, QAbstractAnimation, \
QParallelAnimationGroup
from PySide6.QtGui import QUndoStack, QUndoCommand
from pyzx.utils import vertex_is_w

from .common import VT, GraphT, pos_to_view
from .custom_rule import CustomRule
from .rewrite_data import operations
from .common import VT, GraphT, pos_to_view, ANIMATION_DURATION
from .graphscene import GraphScene
from .vitem import VItem, VItemAnimation, VITEM_UNSELECTED_Z, VITEM_SELECTED_Z, get_w_partner_vitem

if TYPE_CHECKING:
from .proof_panel import ProofPanel
from .rewrite_action import RewriteAction


class AnimatedUndoStack(QUndoStack):
"""An undo stack that can play animations between actions."""
Expand Down Expand Up @@ -183,7 +191,8 @@ def fuse(dragged: VItem, target: VItem, meet_halfway: bool = False) -> QAbstract
if not meet_halfway:
group.addAnimation(move(dragged, target=target.pos(), duration=100, ease=QEasingCurve(QEasingCurve.Type.OutQuad)))
else:
halfway_pos = (dragged.pos() + target.pos()) / 2
sum_pos = dragged.pos() + target.pos()
halfway_pos = QPointF(sum_pos.x() / 2, sum_pos.y() / 2)
group.addAnimation(move(dragged, target=halfway_pos, duration=100, ease=QEasingCurve(QEasingCurve.Type.OutQuad)))
group.addAnimation(move(target, target=halfway_pos, duration=100, ease=QEasingCurve(QEasingCurve.Type.OutQuad)))
group.addAnimation(scale(target, target=1, duration=100, ease=QEasingCurve(QEasingCurve.Type.InBack)))
Expand Down Expand Up @@ -255,3 +264,55 @@ def unfuse(before: GraphT, after: GraphT, src: VT, scene: GraphScene) -> QAbstra
return morph_graph(before, after, scene, to_start=lambda _: src, to_end=lambda _: None,
duration=700, ease=QEasingCurve(QEasingCurve.Type.OutElastic))


def make_animation(self: RewriteAction, panel: ProofPanel, g, matches, rem_verts) -> tuple:
anim_before = None
anim_after = None
if self.name == operations['spider']['text'] or self.name == operations['fuse_w']['text']:
anim_before = QParallelAnimationGroup()
for v1, v2 in matches:
if v1 in rem_verts:
v1, v2 = v2, v1
anim_before.addAnimation(fuse(panel.graph_scene.vertex_map[v2], panel.graph_scene.vertex_map[v1]))
elif self.name == operations['to_z']['text']:
print('To do: animate ' + self.name)
elif self.name == operations['to_x']['text']:
print('To do: animate ' + self.name)
elif self.name == operations['rem_id']['text']:
anim_before = QParallelAnimationGroup()
for m in matches:
anim_before.addAnimation(remove_id(panel.graph_scene.vertex_map[m[0]]))
elif self.name == operations['copy']['text']:
anim_before = QParallelAnimationGroup()
for m in matches:
anim_before.addAnimation(fuse(panel.graph_scene.vertex_map[m[0]],
panel.graph_scene.vertex_map[m[1]]))
anim_after = QParallelAnimationGroup()
for m in matches:
anim_after.addAnimation(strong_comp(panel.graph, g, m[1], panel.graph_scene))
elif self.name == operations['pauli']['text']:
print('To do: animate ' + self.name)
elif self.name == operations['bialgebra']['text']:
anim_before = QParallelAnimationGroup()
for v1, v2 in matches:
anim_before.addAnimation(fuse(panel.graph_scene.vertex_map[v1],
panel.graph_scene.vertex_map[v2], meet_halfway=True))
anim_after = QParallelAnimationGroup()
for v1, v2 in matches:
v2_row, v2_qubit = panel.graph.row(v2), panel.graph.qubit(v2)
panel.graph.set_row(v2, (panel.graph.row(v1) + v2_row) / 2)
panel.graph.set_qubit(v2, (panel.graph.qubit(v1) + v2_qubit) / 2)
anim_after.addAnimation(strong_comp(panel.graph, g, v2, panel.graph_scene))
panel.graph.set_row(v2, v2_row)
panel.graph.set_qubit(v2, v2_qubit)
elif isinstance(self.rule, CustomRule) and self.rule.last_rewrite_center is not None:
center = self.rule.last_rewrite_center
duration = ANIMATION_DURATION / 2
anim_before = morph_graph_to_center(panel.graph, lambda v: v not in g.graph,
panel.graph_scene, center, duration,
QEasingCurve(QEasingCurve.Type.InQuad))
anim_after = morph_graph_from_center(g, lambda v: v not in panel.graph.graph,
panel.graph_scene, center, duration,
QEasingCurve(QEasingCurve.Type.OutQuad))

return anim_before, anim_after
33 changes: 27 additions & 6 deletions zxlive/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,21 @@
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QCommandLineParser
from PySide6.QtGui import QIcon

import sys
from .mainwindow import MainWindow
from .common import get_data
# sys.path.insert(0, '../pyzx') # So that it can find a local copy of pyzx

#sys.path.insert(0, '../pyzx') # So that it can find a local copy of pyzx
from .mainwindow import MainWindow
from .common import get_data, GraphT
from typing import Optional

# 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
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) # type: ignore


class ZXLive(QApplication):
Expand All @@ -39,6 +41,8 @@ class ZXLive(QApplication):
...
"""

main_window: Optional[MainWindow] = None

def __init__(self) -> None:
super().__init__(sys.argv)
self.setApplicationName('ZXLive')
Expand All @@ -62,9 +66,26 @@ def __init__(self) -> None:
for f in parser.positionalArguments():
self.main_window.open_file_from_path(f)

def edit_graph(self, g: GraphT, name: str) -> None:
"""Opens a ZXLive window from within a notebook to edit a graph."""
if not self.main_window:
self.main_window = MainWindow()
self.main_window.show()
self.main_window.open_graph_from_notebook(g, name)

def main() -> None:
"""Main entry point for ZXLive"""
def get_copy_of_graph(self, name: str) -> GraphT:
"""Returns a copy of the graph which has the given name."""
return self.main_window.get_copy_of_graph(name)


def get_embedded_app() -> ZXLive:
"""Main entry point for ZXLive as an embedded app inside a jupyter notebook."""
app = QApplication.instance() or ZXLive()
app.__class__ = ZXLive
return app


def main() -> None:
"""Main entry point for ZXLive as a standalone app."""
zxl = ZXLive()
zxl.exec_()
7 changes: 6 additions & 1 deletion zxlive/base_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ def clear_graph(self) -> None:
cmd = SetGraph(self.graph_view, empty_graph)
self.undo_stack.push(cmd)

def replace_graph(self, graph: GraphT) -> None:
cmd = SetGraph(self.graph_view, graph)
self.undo_stack.push(cmd)

def select_all(self) -> None:
self.graph_scene.select_all()

Expand All @@ -96,7 +100,8 @@ def deselect_all(self) -> None:
def copy_selection(self) -> GraphT:
selection = list(self.graph_scene.selected_vertices)
copied_graph = self.graph.subgraph_from_vertices(selection)
assert isinstance(copied_graph, GraphT)
# Mypy issue: https://github.com/python/mypy/issues/11673
assert isinstance(copied_graph, GraphT) # type: ignore
return copied_graph

def update_colors(self) -> None:
Expand Down
5 changes: 3 additions & 2 deletions zxlive/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from PySide6.QtWidgets import QListView
from pyzx import basicrules
from pyzx.graph import GraphDiff
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
Expand Down Expand Up @@ -304,9 +305,9 @@ def redo(self) -> None:
class ChangePhase(BaseCommand):
"""Updates the phase of a spider."""
v: VT
new_phase: Union[Fraction, int]
new_phase: Union[Fraction, Poly, complex]

_old_phase: Optional[Union[Fraction, int]] = field(default=None, init=False)
_old_phase: Optional[Union[Fraction, Poly, complex]] = field(default=None, init=False)

def undo(self) -> None:
assert self._old_phase is not None
Expand Down
2 changes: 1 addition & 1 deletion zxlive/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def _get_synonyms(key: str, default: list[str]) -> list[str]:


def to_tikz(g: GraphT) -> str:
return pyzx.tikz.to_tikz(g)
return pyzx.tikz.to_tikz(g) # type: ignore

def from_tikz(s: str) -> GraphT:
try:
Expand Down
Loading

0 comments on commit 2bb5f87

Please sign in to comment.