From affa12113a1efbeae6fce634c78d3f6783292ae5 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Tue, 8 Aug 2023 19:34:00 +0100 Subject: [PATCH 01/21] wip: first attempt at adding a custom rewrite --- pyproject.toml | 1 + zxlive/proof_actions.py | 113 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4bfbc770..ff933dc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "PySide6", "pyzx @ git+https://github.com/Quantomatic/pyzx.git", "sympy>=1.12", + "networkx", ] [project.optional-dependencies] diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 829b47b5..ad5bc407 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -5,9 +5,10 @@ from PySide6.QtWidgets import QPushButton, QButtonGroup import pyzx +from pyzx.utils import VertexType, EdgeType from .commands import AddRewriteStep -from .common import VT,ET, GraphT +from .common import VT,ET, GraphT, Graph from . import animations as anims if TYPE_CHECKING: @@ -125,6 +126,108 @@ def update_active(self, g: GraphT, verts: List[VT], edges: List[ET]) -> None: action.update_active(g,verts,edges) +import networkx as nx +from networkx.algorithms import isomorphism + +def to_networkx(graph: Graph, b_order = None): + G = nx.Graph() + if b_order is None: + b_order = {v: -1 for v in graph.vertices()} + G.add_nodes_from([(v, {"type": graph.type(v), "b_order": b_order[v]}) for v in graph.vertices()]) + G.add_edges_from([(*v, {"type": graph.edge_type(v)}) for v in graph.edges()]) + return G + +def bialg_test_matcher(g, if_vertex_in_selection): + verts = [v for v in g.vertices() if if_vertex_in_selection(v)] + bialg1 = get_graph_from_file("bialg1.tikz") + bialg1_nx = to_networkx(bialg1) + g_nx = to_networkx(g) + subgraph_nx = nx.Graph(g_nx.subgraph(verts)) + for v in verts: + for vn in g.neighbors(v): + if vn not in verts: + subgraph_nx.add_node(vn, type=VertexType.BOUNDARY) + subgraph_nx.add_edge(v, vn, type=EdgeType.SIMPLE) + GM = isomorphism.GraphMatcher(bialg1_nx, subgraph_nx,\ + node_match=isomorphism.categorical_node_match(['type'],[1])) + if GM.is_isomorphic(): + return verts + else: + return False + +def bialg_test_rule(g, verts): + bialg1 = get_graph_from_file("bialg1.tikz") + b1_order = {v: -1 for v in bialg1.vertices()} + i = 0 + for v in bialg1.vertices(): + if bialg1.type(v) == VertexType.BOUNDARY: + b1_order[v] = i + i += 1 + bialg2 = get_graph_from_file("bialg2.tikz") + b2_order = {v: -1 for v in bialg2.vertices()} + i = 0 + for v in bialg2.vertices(): + if bialg2.type(v) == VertexType.BOUNDARY: + b2_order[v] = i + i += 1 + g_nx = to_networkx(g) + bialg1_nx = to_networkx(bialg1, b1_order) + bialg2_nx = to_networkx(bialg2, b2_order) + subgraph_nx = nx.Graph(g_nx.subgraph(verts)) + for v in verts: + for vn in g.neighbors(v): + if vn not in verts: + subgraph_nx.add_node(vn, type=VertexType.BOUNDARY) + subgraph_nx.add_edge(v, vn, type=EdgeType.SIMPLE) + GM = isomorphism.GraphMatcher(bialg1_nx, subgraph_nx,\ + node_match=isomorphism.categorical_node_match(['type'],[1])) + + matching = list(GM.match())[0] + + for v in verts: + for vn in g.neighbors(v): + if vn not in verts: + subgraph_nx.add_node(vn, type=VertexType.BOUNDARY) + subgraph_nx.add_edge(v, vn, type=EdgeType.SIMPLE) + + vertices_to_remove = [] + for v in matching: + if subgraph_nx.nodes()[matching[v]]['type'] != VertexType.BOUNDARY: + vertices_to_remove.append(matching[v]) + + boundary_mapping = {} + new_vertices_mapping = {} + for v in bialg2_nx.nodes(): + if bialg2_nx.nodes()[v]['type'] == VertexType.BOUNDARY: + for x, data in bialg1_nx.nodes(data=True): + if data['type'] == VertexType.BOUNDARY and data['b_order'] == bialg2_nx.nodes()[v]['b_order']: + boundary_mapping[v] = matching[x] + break + else: + new_vertices_mapping[v] = g.add_vertex(bialg2_nx.nodes()[v]['type']) + + # create etab to add edges + etab = {} + for v1, v2, data in bialg2_nx.edges(data=True): + if bialg2_nx.nodes()[v1]['type'] == VertexType.BOUNDARY: + v1 = boundary_mapping[v1] + else: + v1 = new_vertices_mapping[v1] + if bialg2_nx.nodes()[v2]['type'] == VertexType.BOUNDARY: + v2 = boundary_mapping[v2] + else: + v2 = new_vertices_mapping[v2] + if (v1, v2) not in etab: etab[(v1, v2)] = [0,0] + etab[(v1, v2)][data['type']-1] += 1 + + return etab, vertices_to_remove, [], True + +def get_graph_from_file(file_path): + with open(file_path, 'r') as f: + data = f.read() + graph = Graph.from_tikz(data) + return graph + spider_fuse = ProofAction.from_dict(operations['spider']) to_z = ProofAction.from_dict(operations['to_z']) to_x = ProofAction.from_dict(operations['to_x']) @@ -132,6 +235,12 @@ def update_active(self, g: GraphT, verts: List[VT], edges: List[ET]) -> None: copy_action = ProofAction.from_dict(operations['copy']) pauli = ProofAction.from_dict(operations['pauli']) bialgebra = ProofAction.from_dict(operations['bialgebra']) +bialg_test = ProofAction.from_dict({"text": "bialg_test", + "tooltip": "bialg_test", + "matcher": bialg_test_matcher, + "rule": bialg_test_rule, + "type": MATCHES_VERTICES}) + -actions_basic = ProofActionGroup(spider_fuse,to_z,to_x,rem_id,copy_action,pauli,bialgebra) +actions_basic = ProofActionGroup(spider_fuse,to_z,to_x,rem_id,copy_action,pauli,bialgebra, bialg_test) From f77f5b535eb10a47fc2795cc29c135302faf90b1 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Tue, 8 Aug 2023 20:05:05 +0100 Subject: [PATCH 02/21] use pyzx vdata to store boundary order --- zxlive/proof_actions.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index ad5bc407..2fece116 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -129,11 +129,13 @@ def update_active(self, g: GraphT, verts: List[VT], edges: List[ET]) -> None: import networkx as nx from networkx.algorithms import isomorphism -def to_networkx(graph: Graph, b_order = None): +def to_networkx(graph: Graph): G = nx.Graph() - if b_order is None: - b_order = {v: -1 for v in graph.vertices()} - G.add_nodes_from([(v, {"type": graph.type(v), "b_order": b_order[v]}) for v in graph.vertices()]) + v_data = {v: {"type": graph.type(v), + "phase": graph.type(v), + "boundary_order": graph.vdata(v, "boundary_order", default=-1),} + for v in graph.vertices()} + G.add_nodes_from([(v, v_data[v]) for v in graph.vertices()]) G.add_edges_from([(*v, {"type": graph.edge_type(v)}) for v in graph.edges()]) return G @@ -157,22 +159,20 @@ def bialg_test_matcher(g, if_vertex_in_selection): def bialg_test_rule(g, verts): bialg1 = get_graph_from_file("bialg1.tikz") - b1_order = {v: -1 for v in bialg1.vertices()} i = 0 for v in bialg1.vertices(): if bialg1.type(v) == VertexType.BOUNDARY: - b1_order[v] = i + bialg1.set_vdata(v, "boundary_order", i) i += 1 bialg2 = get_graph_from_file("bialg2.tikz") - b2_order = {v: -1 for v in bialg2.vertices()} i = 0 for v in bialg2.vertices(): if bialg2.type(v) == VertexType.BOUNDARY: - b2_order[v] = i + bialg2.set_vdata(v, "boundary_order", i) i += 1 g_nx = to_networkx(g) - bialg1_nx = to_networkx(bialg1, b1_order) - bialg2_nx = to_networkx(bialg2, b2_order) + bialg1_nx = to_networkx(bialg1) + bialg2_nx = to_networkx(bialg2) subgraph_nx = nx.Graph(g_nx.subgraph(verts)) for v in verts: for vn in g.neighbors(v): @@ -200,7 +200,8 @@ def bialg_test_rule(g, verts): for v in bialg2_nx.nodes(): if bialg2_nx.nodes()[v]['type'] == VertexType.BOUNDARY: for x, data in bialg1_nx.nodes(data=True): - if data['type'] == VertexType.BOUNDARY and data['b_order'] == bialg2_nx.nodes()[v]['b_order']: + if data['type'] == VertexType.BOUNDARY and \ + data['boundary_order'] == bialg2_nx.nodes()[v]['boundary_order']: boundary_mapping[v] = matching[x] break else: From 43a8e1aae07215b21efce1ee072e00bb02c3e8ec Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Tue, 8 Aug 2023 20:14:42 +0100 Subject: [PATCH 03/21] set boundary_index of boundary node in edit panel --- zxlive/edit_panel.py | 8 ++++++++ zxlive/proof_actions.py | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/zxlive/edit_panel.py b/zxlive/edit_panel.py index adfee8e3..5777a0f1 100644 --- a/zxlive/edit_panel.py +++ b/zxlive/edit_panel.py @@ -166,6 +166,14 @@ def _vert_moved(self, vs: list[tuple[VT, float, float]]) -> None: def _vert_double_clicked(self, v: VT) -> None: if self.graph.type(v) == VertexType.BOUNDARY: + input_, ok = QInputDialog.getText( + self, "Input Dialog", "Enter Boundary Index:" + ) + try: + input_ = int(input_.strip()) + self.graph.set_vdata(v, "boundary_index", input_) + except ValueError: + show_error_msg("Wrong Input Type", "Please enter a valid input (e.g. 1, 2)") return input_, ok = QInputDialog.getText( diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 2fece116..240d723b 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -133,7 +133,7 @@ def to_networkx(graph: Graph): G = nx.Graph() v_data = {v: {"type": graph.type(v), "phase": graph.type(v), - "boundary_order": graph.vdata(v, "boundary_order", default=-1),} + "boundary_index": graph.vdata(v, "boundary_index", default=-1),} for v in graph.vertices()} G.add_nodes_from([(v, v_data[v]) for v in graph.vertices()]) G.add_edges_from([(*v, {"type": graph.edge_type(v)}) for v in graph.edges()]) @@ -162,13 +162,13 @@ def bialg_test_rule(g, verts): i = 0 for v in bialg1.vertices(): if bialg1.type(v) == VertexType.BOUNDARY: - bialg1.set_vdata(v, "boundary_order", i) + bialg1.set_vdata(v, "boundary_index", i) i += 1 bialg2 = get_graph_from_file("bialg2.tikz") i = 0 for v in bialg2.vertices(): if bialg2.type(v) == VertexType.BOUNDARY: - bialg2.set_vdata(v, "boundary_order", i) + bialg2.set_vdata(v, "boundary_index", i) i += 1 g_nx = to_networkx(g) bialg1_nx = to_networkx(bialg1) @@ -201,7 +201,7 @@ def bialg_test_rule(g, verts): if bialg2_nx.nodes()[v]['type'] == VertexType.BOUNDARY: for x, data in bialg1_nx.nodes(data=True): if data['type'] == VertexType.BOUNDARY and \ - data['boundary_order'] == bialg2_nx.nodes()[v]['boundary_order']: + data['boundary_index'] == bialg2_nx.nodes()[v]['boundary_index']: boundary_mapping[v] = matching[x] break else: From 6a62ea4c1d41e0775de0349f953d5341004b91b0 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Tue, 8 Aug 2023 20:34:52 +0100 Subject: [PATCH 04/21] use the boundary_index data from the file --- zxlive/proof_actions.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 240d723b..714bcc01 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -141,7 +141,7 @@ def to_networkx(graph: Graph): def bialg_test_matcher(g, if_vertex_in_selection): verts = [v for v in g.vertices() if if_vertex_in_selection(v)] - bialg1 = get_graph_from_file("bialg1.tikz") + bialg1 = get_graph_from_file("bialg1.json") bialg1_nx = to_networkx(bialg1) g_nx = to_networkx(g) subgraph_nx = nx.Graph(g_nx.subgraph(verts)) @@ -158,18 +158,8 @@ def bialg_test_matcher(g, if_vertex_in_selection): return False def bialg_test_rule(g, verts): - bialg1 = get_graph_from_file("bialg1.tikz") - i = 0 - for v in bialg1.vertices(): - if bialg1.type(v) == VertexType.BOUNDARY: - bialg1.set_vdata(v, "boundary_index", i) - i += 1 - bialg2 = get_graph_from_file("bialg2.tikz") - i = 0 - for v in bialg2.vertices(): - if bialg2.type(v) == VertexType.BOUNDARY: - bialg2.set_vdata(v, "boundary_index", i) - i += 1 + bialg1 = get_graph_from_file("bialg1.json") + bialg2 = get_graph_from_file("bialg2.json") g_nx = to_networkx(g) bialg1_nx = to_networkx(bialg1) bialg2_nx = to_networkx(bialg2) @@ -226,7 +216,7 @@ def bialg_test_rule(g, verts): def get_graph_from_file(file_path): with open(file_path, 'r') as f: data = f.read() - graph = Graph.from_tikz(data) + graph = Graph.from_json(data) return graph spider_fuse = ProofAction.from_dict(operations['spider']) From a2ebd373486a6c21efdba89200d48bcccc1774c7 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Tue, 8 Aug 2023 20:43:09 +0100 Subject: [PATCH 05/21] rewrites now take phase into account --- zxlive/proof_actions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 714bcc01..f3b8dd6c 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -132,7 +132,7 @@ def update_active(self, g: GraphT, verts: List[VT], edges: List[ET]) -> None: def to_networkx(graph: Graph): G = nx.Graph() v_data = {v: {"type": graph.type(v), - "phase": graph.type(v), + "phase": graph.phase(v), "boundary_index": graph.vdata(v, "boundary_index", default=-1),} for v in graph.vertices()} G.add_nodes_from([(v, v_data[v]) for v in graph.vertices()]) @@ -151,7 +151,7 @@ def bialg_test_matcher(g, if_vertex_in_selection): subgraph_nx.add_node(vn, type=VertexType.BOUNDARY) subgraph_nx.add_edge(v, vn, type=EdgeType.SIMPLE) GM = isomorphism.GraphMatcher(bialg1_nx, subgraph_nx,\ - node_match=isomorphism.categorical_node_match(['type'],[1])) + node_match=isomorphism.categorical_node_match(['type', 'phase'],[1, 0])) if GM.is_isomorphic(): return verts else: @@ -170,7 +170,7 @@ def bialg_test_rule(g, verts): subgraph_nx.add_node(vn, type=VertexType.BOUNDARY) subgraph_nx.add_edge(v, vn, type=EdgeType.SIMPLE) GM = isomorphism.GraphMatcher(bialg1_nx, subgraph_nx,\ - node_match=isomorphism.categorical_node_match(['type'],[1])) + node_match=isomorphism.categorical_node_match(['type', 'phase'],[1, 0])) matching = list(GM.match())[0] From 51f5b1f06a381075b205e922083f93d485154572 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Tue, 8 Aug 2023 21:01:05 +0100 Subject: [PATCH 06/21] method to create custom rule using left & right --- zxlive/proof_actions.py | 45 ++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index f3b8dd6c..989d174c 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -139,10 +139,9 @@ def to_networkx(graph: Graph): G.add_edges_from([(*v, {"type": graph.edge_type(v)}) for v in graph.edges()]) return G -def bialg_test_matcher(g, if_vertex_in_selection): +def custom_matcher(g, if_vertex_in_selection, left): verts = [v for v in g.vertices() if if_vertex_in_selection(v)] - bialg1 = get_graph_from_file("bialg1.json") - bialg1_nx = to_networkx(bialg1) + bialg1_nx = to_networkx(left) g_nx = to_networkx(g) subgraph_nx = nx.Graph(g_nx.subgraph(verts)) for v in verts: @@ -157,19 +156,17 @@ def bialg_test_matcher(g, if_vertex_in_selection): else: return False -def bialg_test_rule(g, verts): - bialg1 = get_graph_from_file("bialg1.json") - bialg2 = get_graph_from_file("bialg2.json") +def custom_rule(g, verts, left, right): g_nx = to_networkx(g) - bialg1_nx = to_networkx(bialg1) - bialg2_nx = to_networkx(bialg2) + left_nx = to_networkx(left) + right_nx = to_networkx(right) subgraph_nx = nx.Graph(g_nx.subgraph(verts)) for v in verts: for vn in g.neighbors(v): if vn not in verts: subgraph_nx.add_node(vn, type=VertexType.BOUNDARY) subgraph_nx.add_edge(v, vn, type=EdgeType.SIMPLE) - GM = isomorphism.GraphMatcher(bialg1_nx, subgraph_nx,\ + GM = isomorphism.GraphMatcher(left_nx, subgraph_nx,\ node_match=isomorphism.categorical_node_match(['type', 'phase'],[1, 0])) matching = list(GM.match())[0] @@ -187,24 +184,24 @@ def bialg_test_rule(g, verts): boundary_mapping = {} new_vertices_mapping = {} - for v in bialg2_nx.nodes(): - if bialg2_nx.nodes()[v]['type'] == VertexType.BOUNDARY: - for x, data in bialg1_nx.nodes(data=True): + for v in right_nx.nodes(): + if right_nx.nodes()[v]['type'] == VertexType.BOUNDARY: + for x, data in left_nx.nodes(data=True): if data['type'] == VertexType.BOUNDARY and \ - data['boundary_index'] == bialg2_nx.nodes()[v]['boundary_index']: + data['boundary_index'] == right_nx.nodes()[v]['boundary_index']: boundary_mapping[v] = matching[x] break else: - new_vertices_mapping[v] = g.add_vertex(bialg2_nx.nodes()[v]['type']) + new_vertices_mapping[v] = g.add_vertex(right_nx.nodes()[v]['type']) # create etab to add edges etab = {} - for v1, v2, data in bialg2_nx.edges(data=True): - if bialg2_nx.nodes()[v1]['type'] == VertexType.BOUNDARY: + for v1, v2, data in right_nx.edges(data=True): + if right_nx.nodes()[v1]['type'] == VertexType.BOUNDARY: v1 = boundary_mapping[v1] else: v1 = new_vertices_mapping[v1] - if bialg2_nx.nodes()[v2]['type'] == VertexType.BOUNDARY: + if right_nx.nodes()[v2]['type'] == VertexType.BOUNDARY: v2 = boundary_mapping[v2] else: v2 = new_vertices_mapping[v2] @@ -219,6 +216,14 @@ def get_graph_from_file(file_path): graph = Graph.from_json(data) return graph + +def create_custom_matcher(left): + return lambda g, selection: custom_matcher(g, selection, left) + +def create_custom_rule(left, right): + return lambda g, verts: custom_rule(g, verts, left, right) + + spider_fuse = ProofAction.from_dict(operations['spider']) to_z = ProofAction.from_dict(operations['to_z']) to_x = ProofAction.from_dict(operations['to_x']) @@ -226,10 +231,12 @@ def get_graph_from_file(file_path): copy_action = ProofAction.from_dict(operations['copy']) pauli = ProofAction.from_dict(operations['pauli']) bialgebra = ProofAction.from_dict(operations['bialgebra']) +left = get_graph_from_file("bialg1.json") +right = get_graph_from_file("bialg2.json") bialg_test = ProofAction.from_dict({"text": "bialg_test", "tooltip": "bialg_test", - "matcher": bialg_test_matcher, - "rule": bialg_test_rule, + "matcher": create_custom_matcher(left), + "rule": create_custom_rule(left, right), "type": MATCHES_VERTICES}) From 8918b5af3f8e7738f02489e305f97c5cb32e21d9 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Tue, 8 Aug 2023 21:18:25 +0100 Subject: [PATCH 07/21] code clean up --- zxlive/mainwindow.py | 2 +- zxlive/proof_actions.py | 76 ++++++++++++++++++----------------------- 2 files changed, 35 insertions(+), 43 deletions(-) diff --git a/zxlive/mainwindow.py b/zxlive/mainwindow.py index c5ce4fd8..a78081ad 100644 --- a/zxlive/mainwindow.py +++ b/zxlive/mainwindow.py @@ -229,7 +229,7 @@ def close_action(self) -> bool: self.close() if not self.active_panel.undo_stack.isClean(): name = self.tab_widget.tabText(i).replace("*","") - answer = QMessageBox.question(self, "Save Changes", + answer = QMessageBox.question(self, "Save Changes", f"Do you wish to save your changes to {name} before closing?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel) if answer == QMessageBox.StandardButton.Cancel: return False diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 989d174c..9a5e0935 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -1,15 +1,17 @@ import copy from dataclasses import dataclass, field, replace -from typing import Callable, Literal, List, Optional, Final, TYPE_CHECKING - -from PySide6.QtWidgets import QPushButton, QButtonGroup +from typing import Callable, Literal, List, Optional, TYPE_CHECKING +import networkx as nx +from networkx.algorithms import isomorphism import pyzx from pyzx.utils import VertexType, EdgeType -from .commands import AddRewriteStep -from .common import VT,ET, GraphT, Graph +from PySide6.QtWidgets import QPushButton, QButtonGroup + from . import animations as anims +from .commands import AddRewriteStep +from .common import ET, Graph, GraphT, VT if TYPE_CHECKING: from .proof_panel import ProofPanel @@ -26,15 +28,15 @@ @dataclass class ProofAction(object): name: str - matcher: Callable[[GraphT,Callable],List] - rule: Callable[[GraphT,List],pyzx.rules.RewriteOutputType[ET,VT]] + matcher: Callable[[GraphT,Callable], List] + rule: Callable[[GraphT,List], pyzx.rules.RewriteOutputType[ET,VT]] match_type: MatchType tooltip: str button: Optional[QPushButton] = field(default=None, init=False) @classmethod def from_dict(cls, d: dict) -> "ProofAction": - return cls(d['text'],d['matcher'],d['rule'],d['type'],d['tooltip']) + return cls(d['text'], d['matcher'], d['rule'], d['type'], d['tooltip']) def do_rewrite(self,panel: "ProofPanel") -> None: verts, edges = panel.parse_selection() @@ -113,21 +115,20 @@ def rewriter() -> None: return rewriter for action in self.actions: if action.button is not None: continue - btn = QPushButton(action.name,parent) + btn = QPushButton(action.name, parent) btn.setMaximumWidth(150) btn.setStatusTip(action.tooltip) btn.setEnabled(False) - btn.clicked.connect(create_rewrite(action,parent)) + btn.clicked.connect(create_rewrite(action, parent)) self.btn_group.addButton(btn) action.button = btn def update_active(self, g: GraphT, verts: List[VT], edges: List[ET]) -> None: for action in self.actions: - action.update_active(g,verts,edges) + action.update_active(g, verts, edges) + -import networkx as nx -from networkx.algorithms import isomorphism def to_networkx(graph: Graph): G = nx.Graph() @@ -135,47 +136,38 @@ def to_networkx(graph: Graph): "phase": graph.phase(v), "boundary_index": graph.vdata(v, "boundary_index", default=-1),} for v in graph.vertices()} - G.add_nodes_from([(v, v_data[v]) for v in graph.vertices()]) + G.add_nodes_from([(v, v_data[v]) for v in graph.vertices()]) G.add_edges_from([(*v, {"type": graph.edge_type(v)}) for v in graph.edges()]) return G -def custom_matcher(g, if_vertex_in_selection, left): - verts = [v for v in g.vertices() if if_vertex_in_selection(v)] - bialg1_nx = to_networkx(left) - g_nx = to_networkx(g) - subgraph_nx = nx.Graph(g_nx.subgraph(verts)) +def create_subgraph(graph, verts): + graph_nx = to_networkx(graph) + subgraph_nx = nx.Graph(graph_nx.subgraph(verts)) for v in verts: - for vn in g.neighbors(v): + for vn in graph.neighbors(v): if vn not in verts: subgraph_nx.add_node(vn, type=VertexType.BOUNDARY) subgraph_nx.add_edge(v, vn, type=EdgeType.SIMPLE) - GM = isomorphism.GraphMatcher(bialg1_nx, subgraph_nx,\ + return subgraph_nx + +def custom_matcher(graph, if_vertex_in_selection, left): + verts = [v for v in graph.vertices() if if_vertex_in_selection(v)] + left_nx = to_networkx(left) + subgraph_nx = create_subgraph(graph, verts) + graph_matcher = isomorphism.GraphMatcher(left_nx, subgraph_nx,\ node_match=isomorphism.categorical_node_match(['type', 'phase'],[1, 0])) - if GM.is_isomorphic(): + if graph_matcher.is_isomorphic(): return verts - else: - return False + return False -def custom_rule(g, verts, left, right): - g_nx = to_networkx(g) +def custom_rule(graph, vertices, left, right): left_nx = to_networkx(left) right_nx = to_networkx(right) - subgraph_nx = nx.Graph(g_nx.subgraph(verts)) - for v in verts: - for vn in g.neighbors(v): - if vn not in verts: - subgraph_nx.add_node(vn, type=VertexType.BOUNDARY) - subgraph_nx.add_edge(v, vn, type=EdgeType.SIMPLE) - GM = isomorphism.GraphMatcher(left_nx, subgraph_nx,\ - node_match=isomorphism.categorical_node_match(['type', 'phase'],[1, 0])) + subgraph_nx = create_subgraph(graph, vertices) - matching = list(GM.match())[0] - - for v in verts: - for vn in g.neighbors(v): - if vn not in verts: - subgraph_nx.add_node(vn, type=VertexType.BOUNDARY) - subgraph_nx.add_edge(v, vn, type=EdgeType.SIMPLE) + graph_matcher = isomorphism.GraphMatcher(left_nx, subgraph_nx,\ + node_match=isomorphism.categorical_node_match(['type', 'phase'],[1, 0])) + matching = list(graph_matcher.match())[0] vertices_to_remove = [] for v in matching: @@ -192,7 +184,7 @@ def custom_rule(g, verts, left, right): boundary_mapping[v] = matching[x] break else: - new_vertices_mapping[v] = g.add_vertex(right_nx.nodes()[v]['type']) + new_vertices_mapping[v] = graph.add_vertex(right_nx.nodes()[v]['type']) # create etab to add edges etab = {} From edda747e3b457487d98a3fbe57b2557594a08996 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Tue, 8 Aug 2023 22:53:17 +0100 Subject: [PATCH 08/21] dialog box to create new rewrite --- zxlive/mainwindow.py | 56 +++++++++++++++++++++++++++++++++++++---- zxlive/proof_actions.py | 10 ++------ zxlive/proof_panel.py | 4 +-- 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/zxlive/mainwindow.py b/zxlive/mainwindow.py index a78081ad..05c6b858 100644 --- a/zxlive/mainwindow.py +++ b/zxlive/mainwindow.py @@ -20,9 +20,11 @@ from PySide6.QtCore import QFile, QFileInfo, QTextStream, QIODevice, QSettings, QByteArray, QEvent from PySide6.QtGui import QAction, QShortcut, QKeySequence, QCloseEvent -from PySide6.QtWidgets import QMessageBox, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QFileDialog, QSizePolicy +from PySide6.QtWidgets import QMessageBox, QMainWindow, QWidget, QVBoxLayout, QTabWidget, QDialog, QFormLayout, QLineEdit, QTextEdit, QPushButton, QDialogButtonBox from pyzx.graph.base import BaseGraph +from zxlive import proof_actions + from .commands import AddRewriteStep from .base_panel import BasePanel @@ -83,13 +85,13 @@ def __init__(self) -> None: close_action = self._new_action("Close", self.close_action, QKeySequence.StandardKey.Close, "Closes the window") close_action.setShortcuts([QKeySequence(QKeySequence.StandardKey.Close), QKeySequence("Ctrl+W")]) - # TODO: We should remember if we have saved the diagram before, + # TODO: We should remember if we have saved the diagram before, # and give an open to overwrite this file with a Save action save_file = self._new_action("&Save", self.save_file, QKeySequence.StandardKey.Save, "Save the diagram by overwriting the previous loaded file.") save_as = self._new_action("Save &as...", self.save_as, QKeySequence.StandardKey.SaveAs, "Opens a file-picker dialog to save the diagram in a chosen file format") - + file_menu = menu.addMenu("&File") file_menu.addAction(new_graph) file_menu.addAction(open_file) @@ -143,6 +145,10 @@ def __init__(self) -> None: view_menu.addAction(zoom_out) view_menu.addAction(fit_view) + create_new_rewrite = self._new_action("Create new rewrite", self.create_new_rewrite, None, "Create a new rewrite") + rewrite_menu = menu.addMenu("&Rewrite") + rewrite_menu.addAction(create_new_rewrite) + simplify_actions = [] for simp in simplifications.values(): simplify_actions.append(self._new_action(simp["text"], self.apply_pyzx_reduction(simp), None, simp["tool_tip"])) @@ -174,7 +180,7 @@ def active_panel(self) -> Optional[BasePanel]: def closeEvent(self, e: QCloseEvent) -> None: while self.active_panel is not None: # We close all the tabs and ask the user if they want to save progress success = self.close_action() - if not success: + if not success: e.ignore() # Abort the closing return @@ -350,7 +356,47 @@ def reduce() -> None: cmd = AddRewriteStep(self.active_panel.graph_view, new_graph, self.active_panel.step_view, reduction["text"]) self.active_panel.undo_stack.push(cmd) return reduce - + + def create_new_rewrite(self) -> None: + dialog = QDialog() + self.rewrite_form = QFormLayout(dialog) + name = QLineEdit() + self.rewrite_form.addRow("Name", name) + description = QTextEdit() + self.rewrite_form.addRow("Description", description) + left_button = QPushButton("Left hand side of the rule") + right_button = QPushButton("Right hand side of the rule") + self.left_graph = None + self.right_graph = None + def get_file(self, button, side) -> None: + out = import_diagram_dialog(self) + if out is not None: + button.setText(out.file_path) + if side == "left": + self.left_graph = out.g + else: + self.right_graph = out.g + left_button.clicked.connect(lambda: get_file(self, left_button, "left")) + right_button.clicked.connect(lambda: get_file(self, right_button, "right")) + self.rewrite_form.addRow(left_button) + self.rewrite_form.addRow(right_button) + button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) + self.rewrite_form.addRow(button_box) + def add_rewrite() -> None: + if self.left_graph is None or self.right_graph is None: + return + rewrite = proof_actions.ProofAction.from_dict({ + "text":name.text(), + "tooltip":description.toPlainText(), + "matcher": proof_actions.create_custom_matcher(self.left_graph), + "rule": proof_actions.create_custom_rule(self.left_graph, self.right_graph), + "type": proof_actions.MATCHES_VERTICES, + }) + proof_actions.rewrites.append(rewrite) + dialog.accept() + button_box.accepted.connect(add_rewrite) + button_box.rejected.connect(dialog.reject) + if not dialog.exec(): return class SimpEntry(TypedDict): text: str diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 9a5e0935..d1b07c5f 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -223,14 +223,8 @@ def create_custom_rule(left, right): copy_action = ProofAction.from_dict(operations['copy']) pauli = ProofAction.from_dict(operations['pauli']) bialgebra = ProofAction.from_dict(operations['bialgebra']) -left = get_graph_from_file("bialg1.json") -right = get_graph_from_file("bialg2.json") -bialg_test = ProofAction.from_dict({"text": "bialg_test", - "tooltip": "bialg_test", - "matcher": create_custom_matcher(left), - "rule": create_custom_rule(left, right), - "type": MATCHES_VERTICES}) +rewrites = [spider_fuse, to_z, to_x, rem_id, copy_action, pauli,bialgebra] -actions_basic = ProofActionGroup(spider_fuse,to_z,to_x,rem_id,copy_action,pauli,bialgebra, bialg_test) +actions_basic = ProofActionGroup(*rewrites) diff --git a/zxlive/proof_panel.py b/zxlive/proof_panel.py index e1b8a756..e12990c5 100644 --- a/zxlive/proof_panel.py +++ b/zxlive/proof_panel.py @@ -79,7 +79,7 @@ def _toolbar_sections(self) -> Iterator[ToolbarSection]: yield ToolbarSection(*self.identity_choice, exclusive=True) def init_action_groups(self) -> None: - self.action_groups = [proof_actions.actions_basic.copy()] + self.action_groups = [proof_actions.ProofActionGroup(*proof_actions.rewrites)] for group in reversed(self.action_groups): hlayout = QHBoxLayout() group.init_buttons(self) @@ -90,7 +90,7 @@ def init_action_groups(self) -> None: widget = QWidget() widget.setLayout(hlayout) - self.layout().insertWidget(1,widget) + self.layout().insertWidget(1, widget) def parse_selection(self) -> tuple[list[VT], list[ET]]: selection = list(self.graph_scene.selected_vertices) From 232a6b12fa6800f0a309b30d7e6d75ea21ac8107 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Tue, 8 Aug 2023 23:37:29 +0100 Subject: [PATCH 09/21] show index of boundary nodes --- zxlive/proof_actions.py | 4 ++-- zxlive/vitem.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index d1b07c5f..4a07fd12 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -197,7 +197,7 @@ def custom_rule(graph, vertices, left, right): v2 = boundary_mapping[v2] else: v2 = new_vertices_mapping[v2] - if (v1, v2) not in etab: etab[(v1, v2)] = [0,0] + if (v1, v2) not in etab: etab[(v1, v2)] = [0, 0] etab[(v1, v2)][data['type']-1] += 1 return etab, vertices_to_remove, [], True @@ -224,7 +224,7 @@ def create_custom_rule(left, right): pauli = ProofAction.from_dict(operations['pauli']) bialgebra = ProofAction.from_dict(operations['bialgebra']) -rewrites = [spider_fuse, to_z, to_x, rem_id, copy_action, pauli,bialgebra] +rewrites = [spider_fuse, to_z, to_x, rem_id, copy_action, pauli, bialgebra] actions_basic = ProofActionGroup(*rewrites) diff --git a/zxlive/vitem.py b/zxlive/vitem.py index d8c842c1..bb025474 100644 --- a/zxlive/vitem.py +++ b/zxlive/vitem.py @@ -64,7 +64,7 @@ class VItem(QGraphicsPathItem): phase_item: PhaseItem adj_items: Set[EItem] # Connected edges graph_scene: GraphScene - + halftone = "1000100010001000" #QPixmap("images/halftone.png") # Set of animations that are currently running on this vertex @@ -108,7 +108,7 @@ def __init__(self, graph_scene: GraphScene, v: VT) -> None: path = QPainterPath() if self.g.type(self.v) == VertexType.H_BOX: path.addRect(-0.2 * SCALE, -0.2 * SCALE, 0.4 * SCALE, 0.4 * SCALE) - else: + else: path.addEllipse(-0.2 * SCALE, -0.2 * SCALE, 0.4 * SCALE, 0.4 * SCALE) self.setPath(path) self.refresh() @@ -180,7 +180,7 @@ def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: Opt # we intercept the selected option here. option.state &= ~QStyle.StateFlag.State_Selected super().paint(painter, option, widget) - + def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: Any) -> Any: # Snap items to grid on movement by intercepting the position-change # event and returning a new position @@ -360,5 +360,7 @@ def refresh(self) -> None: phase = self.v_item.g.phase(self.v_item.v) # phase = self.v_item.v self.setPlainText(phase_to_s(phase, self.v_item.g.type(self.v_item.v))) + if self.v_item.g.type(self.v_item.v) == VertexType.BOUNDARY: + self.setPlainText(str(self.v_item.g.vdata(self.v_item.v, "boundary_index", ""))) p = self.v_item.pos() self.setPos(p.x(), p.y() - 0.6 * SCALE) From d22e78f3da8a058ceac329d65195da95685e969c Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Wed, 9 Aug 2023 09:27:13 +0100 Subject: [PATCH 10/21] simplified custom_rule function --- zxlive/proof_actions.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 4a07fd12..33e58738 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -174,29 +174,22 @@ def custom_rule(graph, vertices, left, right): if subgraph_nx.nodes()[matching[v]]['type'] != VertexType.BOUNDARY: vertices_to_remove.append(matching[v]) - boundary_mapping = {} - new_vertices_mapping = {} + vertex_map = {} for v in right_nx.nodes(): if right_nx.nodes()[v]['type'] == VertexType.BOUNDARY: for x, data in left_nx.nodes(data=True): if data['type'] == VertexType.BOUNDARY and \ data['boundary_index'] == right_nx.nodes()[v]['boundary_index']: - boundary_mapping[v] = matching[x] + vertex_map[v] = matching[x] break else: - new_vertices_mapping[v] = graph.add_vertex(right_nx.nodes()[v]['type']) + vertex_map[v] = graph.add_vertex(right_nx.nodes()[v]['type']) # create etab to add edges etab = {} for v1, v2, data in right_nx.edges(data=True): - if right_nx.nodes()[v1]['type'] == VertexType.BOUNDARY: - v1 = boundary_mapping[v1] - else: - v1 = new_vertices_mapping[v1] - if right_nx.nodes()[v2]['type'] == VertexType.BOUNDARY: - v2 = boundary_mapping[v2] - else: - v2 = new_vertices_mapping[v2] + v1 = vertex_map[v1] + v2 = vertex_map[v2] if (v1, v2) not in etab: etab[(v1, v2)] = [0, 0] etab[(v1, v2)][data['type']-1] += 1 From 3bd609e47718943b93e7acaee58a8b10a04060bc Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Wed, 9 Aug 2023 09:56:59 +0100 Subject: [PATCH 11/21] code clean up and types --- zxlive/proof_actions.py | 62 +++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 33e58738..0498e800 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -3,7 +3,7 @@ from typing import Callable, Literal, List, Optional, TYPE_CHECKING import networkx as nx -from networkx.algorithms import isomorphism +from networkx.algorithms.isomorphism import GraphMatcher, categorical_node_match import pyzx from pyzx.utils import VertexType, EdgeType @@ -28,8 +28,8 @@ @dataclass class ProofAction(object): name: str - matcher: Callable[[GraphT,Callable], List] - rule: Callable[[GraphT,List], pyzx.rules.RewriteOutputType[ET,VT]] + matcher: Callable[[GraphT, Callable], List] + rule: Callable[[GraphT, List], pyzx.rules.RewriteOutputType[ET,VT]] match_type: MatchType tooltip: str button: Optional[QPushButton] = field(default=None, init=False) @@ -128,9 +128,7 @@ def update_active(self, g: GraphT, verts: List[VT], edges: List[ET]) -> None: action.update_active(g, verts, edges) - - -def to_networkx(graph: Graph): +def to_networkx(graph: Graph) -> nx.Graph: G = nx.Graph() v_data = {v: {"type": graph.type(v), "phase": graph.phase(v), @@ -140,7 +138,7 @@ def to_networkx(graph: Graph): G.add_edges_from([(*v, {"type": graph.edge_type(v)}) for v in graph.edges()]) return G -def create_subgraph(graph, verts): +def create_subgraph(graph: Graph, verts: List[VT]) -> nx.Graph: graph_nx = to_networkx(graph) subgraph_nx = nx.Graph(graph_nx.subgraph(verts)) for v in verts: @@ -150,23 +148,19 @@ def create_subgraph(graph, verts): subgraph_nx.add_edge(v, vn, type=EdgeType.SIMPLE) return subgraph_nx -def custom_matcher(graph, if_vertex_in_selection, left): - verts = [v for v in graph.vertices() if if_vertex_in_selection(v)] - left_nx = to_networkx(left) +def custom_matcher(graph: Graph, in_selection: Callable[[VT], bool], lhs_graph: nx.Graph) -> List[VT]: + verts = [v for v in graph.vertices() if in_selection(v)] subgraph_nx = create_subgraph(graph, verts) - graph_matcher = isomorphism.GraphMatcher(left_nx, subgraph_nx,\ - node_match=isomorphism.categorical_node_match(['type', 'phase'],[1, 0])) + graph_matcher = GraphMatcher(lhs_graph, subgraph_nx,\ + node_match=categorical_node_match(['type', 'phase'], default=[1, 0])) if graph_matcher.is_isomorphic(): return verts - return False + return [] -def custom_rule(graph, vertices, left, right): - left_nx = to_networkx(left) - right_nx = to_networkx(right) +def custom_rule(graph: Graph, vertices: List[VT], lhs_graph: nx.Graph, rhs_graph: nx.Graph) -> pyzx.rules.RewriteOutputType[ET,VT]: subgraph_nx = create_subgraph(graph, vertices) - - graph_matcher = isomorphism.GraphMatcher(left_nx, subgraph_nx,\ - node_match=isomorphism.categorical_node_match(['type', 'phase'],[1, 0])) + graph_matcher = GraphMatcher(lhs_graph, subgraph_nx,\ + node_match=categorical_node_match(['type', 'phase'], default=[1, 0])) matching = list(graph_matcher.match())[0] vertices_to_remove = [] @@ -175,19 +169,19 @@ def custom_rule(graph, vertices, left, right): vertices_to_remove.append(matching[v]) vertex_map = {} - for v in right_nx.nodes(): - if right_nx.nodes()[v]['type'] == VertexType.BOUNDARY: - for x, data in left_nx.nodes(data=True): + for v in rhs_graph.nodes(): + if rhs_graph.nodes()[v]['type'] == VertexType.BOUNDARY: + for x, data in lhs_graph.nodes(data=True): if data['type'] == VertexType.BOUNDARY and \ - data['boundary_index'] == right_nx.nodes()[v]['boundary_index']: + data['boundary_index'] == rhs_graph.nodes()[v]['boundary_index']: vertex_map[v] = matching[x] break else: - vertex_map[v] = graph.add_vertex(right_nx.nodes()[v]['type']) + vertex_map[v] = graph.add_vertex(rhs_graph.nodes()[v]['type']) # create etab to add edges etab = {} - for v1, v2, data in right_nx.edges(data=True): + for v1, v2, data in rhs_graph.edges(data=True): v1 = vertex_map[v1] v2 = vertex_map[v2] if (v1, v2) not in etab: etab[(v1, v2)] = [0, 0] @@ -195,18 +189,11 @@ def custom_rule(graph, vertices, left, right): return etab, vertices_to_remove, [], True -def get_graph_from_file(file_path): - with open(file_path, 'r') as f: - data = f.read() - graph = Graph.from_json(data) - return graph - +def create_custom_matcher(lhs_graph: Graph) -> Callable[[Graph, Callable[[VT], bool]], List[VT]]: + return lambda g, selection: custom_matcher(g, selection, to_networkx(lhs_graph)) -def create_custom_matcher(left): - return lambda g, selection: custom_matcher(g, selection, left) - -def create_custom_rule(left, right): - return lambda g, verts: custom_rule(g, verts, left, right) +def create_custom_rule(lhs_graph: Graph,rhs_graph: Graph) -> Callable[[Graph, List[VT]], pyzx.rules.RewriteOutputType[ET,VT]]: + return lambda g, verts: custom_rule(g, verts, to_networkx(lhs_graph), to_networkx(rhs_graph)) spider_fuse = ProofAction.from_dict(operations['spider']) @@ -218,6 +205,3 @@ def create_custom_rule(left, right): bialgebra = ProofAction.from_dict(operations['bialgebra']) rewrites = [spider_fuse, to_z, to_x, rem_id, copy_action, pauli, bialgebra] - -actions_basic = ProofActionGroup(*rewrites) - From ab6884a22b9bb995e97c240180cef55d3ff9f258 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Wed, 9 Aug 2023 12:07:59 +0100 Subject: [PATCH 12/21] boundary_mapping: multiple nodes adjacent to same outside node --- zxlive/proof_actions.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 0498e800..97d5f2e1 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -141,16 +141,21 @@ def to_networkx(graph: Graph) -> nx.Graph: def create_subgraph(graph: Graph, verts: List[VT]) -> nx.Graph: graph_nx = to_networkx(graph) subgraph_nx = nx.Graph(graph_nx.subgraph(verts)) + boundary_mapping = {} + i = 0 for v in verts: for vn in graph.neighbors(v): if vn not in verts: - subgraph_nx.add_node(vn, type=VertexType.BOUNDARY) - subgraph_nx.add_edge(v, vn, type=EdgeType.SIMPLE) - return subgraph_nx + boundary_node = 'b' + str(i) + boundary_mapping[boundary_node] = vn + subgraph_nx.add_node(boundary_node, type=VertexType.BOUNDARY) + subgraph_nx.add_edge(v, boundary_node, type=EdgeType.SIMPLE) + i += 1 + return subgraph_nx, boundary_mapping def custom_matcher(graph: Graph, in_selection: Callable[[VT], bool], lhs_graph: nx.Graph) -> List[VT]: verts = [v for v in graph.vertices() if in_selection(v)] - subgraph_nx = create_subgraph(graph, verts) + subgraph_nx, _ = create_subgraph(graph, verts) graph_matcher = GraphMatcher(lhs_graph, subgraph_nx,\ node_match=categorical_node_match(['type', 'phase'], default=[1, 0])) if graph_matcher.is_isomorphic(): @@ -158,7 +163,7 @@ def custom_matcher(graph: Graph, in_selection: Callable[[VT], bool], lhs_graph: return [] def custom_rule(graph: Graph, vertices: List[VT], lhs_graph: nx.Graph, rhs_graph: nx.Graph) -> pyzx.rules.RewriteOutputType[ET,VT]: - subgraph_nx = create_subgraph(graph, vertices) + subgraph_nx, boundary_mapping = create_subgraph(graph, vertices) graph_matcher = GraphMatcher(lhs_graph, subgraph_nx,\ node_match=categorical_node_match(['type', 'phase'], default=[1, 0])) matching = list(graph_matcher.match())[0] @@ -174,7 +179,7 @@ def custom_rule(graph: Graph, vertices: List[VT], lhs_graph: nx.Graph, rhs_graph for x, data in lhs_graph.nodes(data=True): if data['type'] == VertexType.BOUNDARY and \ data['boundary_index'] == rhs_graph.nodes()[v]['boundary_index']: - vertex_map[v] = matching[x] + vertex_map[v] = boundary_mapping[matching[x]] break else: vertex_map[v] = graph.add_vertex(rhs_graph.nodes()[v]['type']) From 589d6eadfa7464208012141e02c0de7319c569c8 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Wed, 9 Aug 2023 20:10:32 +0100 Subject: [PATCH 13/21] moved create_new_rewrite to dialogs.py --- zxlive/dialogs.py | 45 +++++++++++++++++++++++++++++++++++-- zxlive/mainwindow.py | 53 +++++--------------------------------------- 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/zxlive/dialogs.py b/zxlive/dialogs.py index 0895068b..9d593320 100644 --- a/zxlive/dialogs.py +++ b/zxlive/dialogs.py @@ -5,9 +5,10 @@ from dataclasses import dataclass from PySide6.QtCore import QFile, QIODevice, QTextStream -from PySide6.QtWidgets import QWidget, QFileDialog, QMessageBox +from PySide6.QtWidgets import QWidget, QFileDialog, QMessageBox, QDialog, QFormLayout, QLineEdit, QTextEdit, QPushButton, QDialogButtonBox from pyzx import Circuit, extract_circuit from pyzx.graph.base import BaseGraph +from zxlive import proof_actions from zxlive.proof import ProofModel @@ -186,7 +187,6 @@ def export_diagram_dialog(graph: GraphT, parent: QWidget) -> Optional[Tuple[str, return file_path, selected_format - def export_proof_dialog(proof_model: ProofModel, parent: QWidget) -> Optional[Tuple[str, FileFormat]]: file_path_and_format = get_file_path_and_format(parent, FileFormat.ZXProof.filter) if file_path_and_format is None or not file_path_and_format[0]: @@ -196,3 +196,44 @@ def export_proof_dialog(proof_model: ProofModel, parent: QWidget) -> Optional[Tu if not write_to_file(file_path, data): return None return file_path, selected_format + +def create_new_rewrite(parent) -> None: + dialog = QDialog() + parent.rewrite_form = QFormLayout(dialog) + name = QLineEdit() + parent.rewrite_form.addRow("Name", name) + description = QTextEdit() + parent.rewrite_form.addRow("Description", description) + left_button = QPushButton("Left-hand side of the rule") + right_button = QPushButton("Right-hand side of the rule") + parent.left_graph = None + parent.right_graph = None + def get_file(self, button, side) -> None: + out = import_diagram_dialog(self) + if out is not None: + button.setText(out.file_path) + if side == "left": + self.left_graph = out.g + else: + self.right_graph = out.g + left_button.clicked.connect(lambda: get_file(parent, left_button, "left")) + right_button.clicked.connect(lambda: get_file(parent, right_button, "right")) + parent.rewrite_form.addRow(left_button) + parent.rewrite_form.addRow(right_button) + button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) + parent.rewrite_form.addRow(button_box) + def add_rewrite() -> None: + if parent.left_graph is None or parent.right_graph is None: + return + rewrite = proof_actions.ProofAction.from_dict({ + "text":name.text(), + "tooltip":description.toPlainText(), + "matcher": proof_actions.create_custom_matcher(parent.left_graph), + "rule": proof_actions.create_custom_rule(parent.left_graph, parent.right_graph), + "type": proof_actions.MATCHES_VERTICES, + }) + proof_actions.rewrites.append(rewrite) + dialog.accept() + button_box.accepted.connect(add_rewrite) + button_box.rejected.connect(dialog.reject) + if not dialog.exec(): return diff --git a/zxlive/mainwindow.py b/zxlive/mainwindow.py index 05c6b858..bf3790bc 100644 --- a/zxlive/mainwindow.py +++ b/zxlive/mainwindow.py @@ -19,19 +19,16 @@ import copy from PySide6.QtCore import QFile, QFileInfo, QTextStream, QIODevice, QSettings, QByteArray, QEvent -from PySide6.QtGui import QAction, QShortcut, QKeySequence, QCloseEvent -from PySide6.QtWidgets import QMessageBox, QMainWindow, QWidget, QVBoxLayout, QTabWidget, QDialog, QFormLayout, QLineEdit, QTextEdit, QPushButton, QDialogButtonBox +from PySide6.QtGui import QAction, QKeySequence, QCloseEvent +from PySide6.QtWidgets import QMessageBox, QMainWindow, QWidget, QVBoxLayout, QTabWidget from pyzx.graph.base import BaseGraph -from zxlive import proof_actions - from .commands import AddRewriteStep - from .base_panel import BasePanel from .edit_panel import GraphEditPanel from .proof_panel import ProofPanel from .construct import * -from .dialogs import ImportGraphOutput, export_proof_dialog, import_diagram_dialog, export_diagram_dialog, show_error_msg, FileFormat +from .dialogs import ImportGraphOutput, create_new_rewrite, export_proof_dialog, import_diagram_dialog, export_diagram_dialog, show_error_msg, FileFormat from .common import GraphT from pyzx import Graph, simplify, Circuit @@ -145,9 +142,9 @@ def __init__(self) -> None: view_menu.addAction(zoom_out) view_menu.addAction(fit_view) - create_new_rewrite = self._new_action("Create new rewrite", self.create_new_rewrite, None, "Create a new rewrite") + new_rewrite = self._new_action("Create new rewrite", lambda: create_new_rewrite(self), None, "Create a new rewrite") rewrite_menu = menu.addMenu("&Rewrite") - rewrite_menu.addAction(create_new_rewrite) + rewrite_menu.addAction(new_rewrite) simplify_actions = [] for simp in simplifications.values(): @@ -357,46 +354,6 @@ def reduce() -> None: self.active_panel.undo_stack.push(cmd) return reduce - def create_new_rewrite(self) -> None: - dialog = QDialog() - self.rewrite_form = QFormLayout(dialog) - name = QLineEdit() - self.rewrite_form.addRow("Name", name) - description = QTextEdit() - self.rewrite_form.addRow("Description", description) - left_button = QPushButton("Left hand side of the rule") - right_button = QPushButton("Right hand side of the rule") - self.left_graph = None - self.right_graph = None - def get_file(self, button, side) -> None: - out = import_diagram_dialog(self) - if out is not None: - button.setText(out.file_path) - if side == "left": - self.left_graph = out.g - else: - self.right_graph = out.g - left_button.clicked.connect(lambda: get_file(self, left_button, "left")) - right_button.clicked.connect(lambda: get_file(self, right_button, "right")) - self.rewrite_form.addRow(left_button) - self.rewrite_form.addRow(right_button) - button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) - self.rewrite_form.addRow(button_box) - def add_rewrite() -> None: - if self.left_graph is None or self.right_graph is None: - return - rewrite = proof_actions.ProofAction.from_dict({ - "text":name.text(), - "tooltip":description.toPlainText(), - "matcher": proof_actions.create_custom_matcher(self.left_graph), - "rule": proof_actions.create_custom_rule(self.left_graph, self.right_graph), - "type": proof_actions.MATCHES_VERTICES, - }) - proof_actions.rewrites.append(rewrite) - dialog.accept() - button_box.accepted.connect(add_rewrite) - button_box.rejected.connect(dialog.reject) - if not dialog.exec(): return class SimpEntry(TypedDict): text: str From 516d9f08344374cdc19dfa449d002c7de3e41254 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Wed, 9 Aug 2023 22:32:42 +0100 Subject: [PATCH 14/21] need to use .copy on ProofActionGroup --- zxlive/proof_actions.py | 2 +- zxlive/proof_panel.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 97d5f2e1..6d4e35e6 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -38,7 +38,7 @@ class ProofAction(object): def from_dict(cls, d: dict) -> "ProofAction": return cls(d['text'], d['matcher'], d['rule'], d['type'], d['tooltip']) - def do_rewrite(self,panel: "ProofPanel") -> None: + def do_rewrite(self, panel: "ProofPanel") -> None: verts, edges = panel.parse_selection() g = copy.deepcopy(panel.graph_scene.g) diff --git a/zxlive/proof_panel.py b/zxlive/proof_panel.py index e12990c5..480b50a2 100644 --- a/zxlive/proof_panel.py +++ b/zxlive/proof_panel.py @@ -79,7 +79,7 @@ def _toolbar_sections(self) -> Iterator[ToolbarSection]: yield ToolbarSection(*self.identity_choice, exclusive=True) def init_action_groups(self) -> None: - self.action_groups = [proof_actions.ProofActionGroup(*proof_actions.rewrites)] + self.action_groups = [proof_actions.ProofActionGroup(*proof_actions.rewrites).copy()] for group in reversed(self.action_groups): hlayout = QHBoxLayout() group.init_buttons(self) From eb7929f0302c18443d4b4c71b69539a143cb2b55 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Wed, 9 Aug 2023 23:14:39 +0100 Subject: [PATCH 15/21] use g.auto_detect_io() to get input/output instead of manually annotated boundary indices --- zxlive/proof_actions.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 6d4e35e6..63368014 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -131,9 +131,12 @@ def update_active(self, g: GraphT, verts: List[VT], edges: List[ET]) -> None: def to_networkx(graph: Graph) -> nx.Graph: G = nx.Graph() v_data = {v: {"type": graph.type(v), - "phase": graph.phase(v), - "boundary_index": graph.vdata(v, "boundary_index", default=-1),} + "phase": graph.phase(v),} for v in graph.vertices()} + for i, input_vertex in enumerate(graph.inputs()): + v_data[input_vertex]["boundary_index"] = f'input_{i}' + for i, output_vertex in enumerate(graph.outputs()): + v_data[output_vertex]["boundary_index"] = f'output_{i}' G.add_nodes_from([(v, v_data[v]) for v in graph.vertices()]) G.add_edges_from([(*v, {"type": graph.edge_type(v)}) for v in graph.edges()]) return G @@ -195,9 +198,12 @@ def custom_rule(graph: Graph, vertices: List[VT], lhs_graph: nx.Graph, rhs_graph return etab, vertices_to_remove, [], True def create_custom_matcher(lhs_graph: Graph) -> Callable[[Graph, Callable[[VT], bool]], List[VT]]: + lhs_graph.auto_detect_io() return lambda g, selection: custom_matcher(g, selection, to_networkx(lhs_graph)) def create_custom_rule(lhs_graph: Graph,rhs_graph: Graph) -> Callable[[Graph, List[VT]], pyzx.rules.RewriteOutputType[ET,VT]]: + lhs_graph.auto_detect_io() + rhs_graph.auto_detect_io() return lambda g, verts: custom_rule(g, verts, to_networkx(lhs_graph), to_networkx(rhs_graph)) From 5d1bb491d57f9c8451fde00ce743b069f65b4662 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Wed, 9 Aug 2023 23:34:21 +0100 Subject: [PATCH 16/21] set qubit instead of boundary_index --- zxlive/edit_panel.py | 4 ++-- zxlive/vitem.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/zxlive/edit_panel.py b/zxlive/edit_panel.py index 5777a0f1..96acb82e 100644 --- a/zxlive/edit_panel.py +++ b/zxlive/edit_panel.py @@ -167,11 +167,11 @@ def _vert_moved(self, vs: list[tuple[VT, float, float]]) -> None: def _vert_double_clicked(self, v: VT) -> None: if self.graph.type(v) == VertexType.BOUNDARY: input_, ok = QInputDialog.getText( - self, "Input Dialog", "Enter Boundary Index:" + self, "Input Dialog", "Enter Qubit Index:" ) try: input_ = int(input_.strip()) - self.graph.set_vdata(v, "boundary_index", input_) + self.graph.set_qubit(v, input_) except ValueError: show_error_msg("Wrong Input Type", "Please enter a valid input (e.g. 1, 2)") return diff --git a/zxlive/vitem.py b/zxlive/vitem.py index bb025474..7c3ba15e 100644 --- a/zxlive/vitem.py +++ b/zxlive/vitem.py @@ -361,6 +361,6 @@ def refresh(self) -> None: # phase = self.v_item.v self.setPlainText(phase_to_s(phase, self.v_item.g.type(self.v_item.v))) if self.v_item.g.type(self.v_item.v) == VertexType.BOUNDARY: - self.setPlainText(str(self.v_item.g.vdata(self.v_item.v, "boundary_index", ""))) + self.setPlainText(str(int(self.v_item.g.qubit(self.v_item.v)))) p = self.v_item.pos() self.setPos(p.x(), p.y() - 0.6 * SCALE) From 8f0de45ea9735b42d143668a6bd7908840de0afc Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Thu, 10 Aug 2023 15:05:40 +0100 Subject: [PATCH 17/21] automatically position the nodes after the rewrite --- zxlive/proof_actions.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 63368014..97250ccc 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -4,8 +4,10 @@ import networkx as nx from networkx.algorithms.isomorphism import GraphMatcher, categorical_node_match +import numpy as np import pyzx from pyzx.utils import VertexType, EdgeType +from shapely import Polygon from PySide6.QtWidgets import QPushButton, QButtonGroup @@ -176,16 +178,23 @@ def custom_rule(graph: Graph, vertices: List[VT], lhs_graph: nx.Graph, rhs_graph if subgraph_nx.nodes()[matching[v]]['type'] != VertexType.BOUNDARY: vertices_to_remove.append(matching[v]) - vertex_map = {} + boundary_vertex_map = {} for v in rhs_graph.nodes(): if rhs_graph.nodes()[v]['type'] == VertexType.BOUNDARY: for x, data in lhs_graph.nodes(data=True): if data['type'] == VertexType.BOUNDARY and \ data['boundary_index'] == rhs_graph.nodes()[v]['boundary_index']: - vertex_map[v] = boundary_mapping[matching[x]] + boundary_vertex_map[v] = boundary_mapping[matching[x]] break - else: - vertex_map[v] = graph.add_vertex(rhs_graph.nodes()[v]['type']) + + vertex_positions = get_vertex_positions(graph, rhs_graph, boundary_vertex_map) + vertex_map = boundary_vertex_map + for v in rhs_graph.nodes(): + if rhs_graph.nodes()[v]['type'] != VertexType.BOUNDARY: + vertex_map[v] = graph.add_vertex(rhs_graph.nodes()[v]['type'], + vertex_positions[v][0], + vertex_positions[v][1], + rhs_graph.nodes()[v]['phase'],) # create etab to add edges etab = {} @@ -197,6 +206,19 @@ def custom_rule(graph: Graph, vertices: List[VT], lhs_graph: nx.Graph, rhs_graph return etab, vertices_to_remove, [], True +def get_vertex_positions(graph, rhs_graph, boundary_vertex_map): + pos_dict = {v: (graph.qubit(m), graph.row(m)) for v, m in boundary_vertex_map.values()} + coords = np.array(list(pos_dict.values())) + center = np.mean(coords, axis=0) + angles = np.arctan2(coords[:,1]-center[1], coords[:,0]-center[0]) + coords = coords[np.argsort(-angles)] + try: + area = Polygon(coords).area + except: + area = 1 + k = (area ** 0.5) / len(rhs_graph) + return nx.spring_layout(rhs_graph, k=k, pos=pos_dict, fixed=boundary_vertex_map.keys()) + def create_custom_matcher(lhs_graph: Graph) -> Callable[[Graph, Callable[[VT], bool]], List[VT]]: lhs_graph.auto_detect_io() return lambda g, selection: custom_matcher(g, selection, to_networkx(lhs_graph)) From 495749992f5e5fbc37431990c51d6ffccbc83c3d Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Thu, 10 Aug 2023 15:30:02 +0100 Subject: [PATCH 18/21] small change for clarity --- zxlive/proof_actions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 97250ccc..003e7ca4 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -191,10 +191,10 @@ def custom_rule(graph: Graph, vertices: List[VT], lhs_graph: nx.Graph, rhs_graph vertex_map = boundary_vertex_map for v in rhs_graph.nodes(): if rhs_graph.nodes()[v]['type'] != VertexType.BOUNDARY: - vertex_map[v] = graph.add_vertex(rhs_graph.nodes()[v]['type'], - vertex_positions[v][0], - vertex_positions[v][1], - rhs_graph.nodes()[v]['phase'],) + vertex_map[v] = graph.add_vertex(ty = rhs_graph.nodes()[v]['type'], + row = vertex_positions[v][0], + qubit = vertex_positions[v][1], + phase = rhs_graph.nodes()[v]['phase'],) # create etab to add edges etab = {} @@ -207,7 +207,7 @@ def custom_rule(graph: Graph, vertices: List[VT], lhs_graph: nx.Graph, rhs_graph return etab, vertices_to_remove, [], True def get_vertex_positions(graph, rhs_graph, boundary_vertex_map): - pos_dict = {v: (graph.qubit(m), graph.row(m)) for v, m in boundary_vertex_map.values()} + pos_dict = {v: (graph.row(m), graph.qubit(m)) for v, m in boundary_vertex_map.items()} coords = np.array(list(pos_dict.values())) center = np.mean(coords, axis=0) angles = np.arctan2(coords[:,1]-center[1], coords[:,0]-center[0]) From ae8e1fe0edc6eef425cf47783d085809d74b76af Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Thu, 10 Aug 2023 15:35:38 +0100 Subject: [PATCH 19/21] add numpy and shapely as dependencies --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ff933dc9..7861cf29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,8 @@ dependencies = [ "pyzx @ git+https://github.com/Quantomatic/pyzx.git", "sympy>=1.12", "networkx", + numpy, + shapely, ] [project.optional-dependencies] From 6490153c8c86b20a2f50d161f38b53d7ff185f23 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Thu, 10 Aug 2023 15:41:21 +0100 Subject: [PATCH 20/21] typo in the previous commit --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7861cf29..a02cd432 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,8 +32,8 @@ dependencies = [ "pyzx @ git+https://github.com/Quantomatic/pyzx.git", "sympy>=1.12", "networkx", - numpy, - shapely, + "numpy", + "shapely", ] [project.optional-dependencies] From f18ea4ea7a5e7eb4fd888a97c95ae14b37bb69ef Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Thu, 10 Aug 2023 17:11:04 +0100 Subject: [PATCH 21/21] warn when rewrite rule doesn't preserve semantics --- zxlive/dialogs.py | 9 +++++++++ zxlive/proof_actions.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/zxlive/dialogs.py b/zxlive/dialogs.py index 9d593320..e3b5bd9d 100644 --- a/zxlive/dialogs.py +++ b/zxlive/dialogs.py @@ -6,6 +6,7 @@ from PySide6.QtCore import QFile, QIODevice, QTextStream from PySide6.QtWidgets import QWidget, QFileDialog, QMessageBox, QDialog, QFormLayout, QLineEdit, QTextEdit, QPushButton, QDialogButtonBox +import numpy as np from pyzx import Circuit, extract_circuit from pyzx.graph.base import BaseGraph from zxlive import proof_actions @@ -225,6 +226,14 @@ def get_file(self, button, side) -> None: def add_rewrite() -> None: if parent.left_graph is None or parent.right_graph is None: return + parent.left_graph.auto_detect_io() + parent.right_graph.auto_detect_io() + left_matrix, right_matrix = parent.left_graph.to_matrix(), parent.right_graph.to_matrix() + if not np.allclose(left_matrix, right_matrix): + if np.allclose(left_matrix / np.linalg.norm(left_matrix), right_matrix / np.linalg.norm(right_matrix)): + show_error_msg("Warning!", "The left-hand side and right-hand side of the rule differ by a scalar.") + else: + show_error_msg("Warning!", "The left-hand side and right-hand side of the rule have different semantics.") rewrite = proof_actions.ProofAction.from_dict({ "text":name.text(), "tooltip":description.toPlainText(), diff --git a/zxlive/proof_actions.py b/zxlive/proof_actions.py index 003e7ca4..ecde7334 100644 --- a/zxlive/proof_actions.py +++ b/zxlive/proof_actions.py @@ -223,7 +223,7 @@ def create_custom_matcher(lhs_graph: Graph) -> Callable[[Graph, Callable[[VT], b lhs_graph.auto_detect_io() return lambda g, selection: custom_matcher(g, selection, to_networkx(lhs_graph)) -def create_custom_rule(lhs_graph: Graph,rhs_graph: Graph) -> Callable[[Graph, List[VT]], pyzx.rules.RewriteOutputType[ET,VT]]: +def create_custom_rule(lhs_graph: Graph, rhs_graph: Graph) -> Callable[[Graph, List[VT]], pyzx.rules.RewriteOutputType[ET,VT]]: lhs_graph.auto_detect_io() rhs_graph.auto_detect_io() return lambda g, verts: custom_rule(g, verts, to_networkx(lhs_graph), to_networkx(rhs_graph))