From 0d69ed1124f67f25f9be7d34669d7cfcbfe97470 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 15 May 2024 17:57:30 +0100 Subject: [PATCH 01/57] wip(py): hugr builder interface --- hugr-py/src/hugr/hugr.py | 153 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 hugr-py/src/hugr/hugr.py diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py new file mode 100644 index 000000000..0c6135429 --- /dev/null +++ b/hugr-py/src/hugr/hugr.py @@ -0,0 +1,153 @@ +from dataclasses import dataclass + +from collections.abc import Collection, MutableMapping, Hashable +from typing import Iterable, Sequence, Protocol, Generic, TypeVar + +from hugr.serialization.serial_hugr import SerialHugr +from hugr.serialization.ops import OpType as Op, DataflowOp +from hugr.serialization.tys import Type, Qubit + + +L = TypeVar("L", bound=Hashable) +R = TypeVar("R", bound=Hashable) + + +@dataclass(init=False) +class BiMap(MutableMapping, Generic[L, R]): + fwd: dict[L, R] + bck: dict[R, L] + + def __getitem__(self, key: L) -> R: + return self.fwd[key] + + def __setitem__(self, key: L, value: R) -> None: + self.insert_left(key, value) + + def __delitem__(self, key: L) -> None: + self.delete_left(key) + + def __iter__(self): + return iter(self.fwd) + + def __len__(self) -> int: + return len(self.fwd) + + def get_left(self, key: R) -> L | None: + return self.bck.get(key) + + def get_right(self, key: L) -> R | None: + return self.fwd.get(key) + + def insert_left(self, key: L, value: R) -> None: + self.fwd[key] = value + self.bck[value] = key + + def insert_right(self, key: R, value: L) -> None: + self.bck[key] = value + self.fwd[value] = key + + def delete_left(self, key: L) -> None: + del self.bck[self.fwd[key]] + del self.fwd[key] + + def delete_right(self, key: R) -> None: + del self.fwd[self.bck[key]] + del self.bck[key] + + +class ToPort(Protocol): + def to_port(self) -> "OutPort": ... + + +@dataclass(frozen=True, eq=True, order=True) +class Node(ToPort): + idx: int + + def to_port(self) -> "OutPort": + return OutPort(self, 0) + + +@dataclass(frozen=True, eq=True, order=True) +class Port: + node: Node + offset: int + + +@dataclass(frozen=True, eq=True, order=True) +class InPort(Port): + pass + + +@dataclass(frozen=True, eq=True, order=True) +class OutPort(Port, ToPort): + def to_port(self) -> "OutPort": + return self + + +@dataclass() +class NodeData: + weight: Op + in_ports: set[InPort] + out_ports: set[OutPort] + + +@dataclass(init=False) +class Hugr: + root: Node + nodes: dict[Node, NodeData] + links: BiMap[OutPort, InPort] + + def add_node(self, op: Op) -> Node: + node = Node(len(self.nodes)) + self.nodes[node] = NodeData(op, set(), set()) + return node + + def add_link(self, src: OutPort, dst: InPort) -> None: + self.links.insert_left(src, dst) + self.nodes[dst.node].in_ports.add(dst) + self.nodes[src.node].out_ports.add(src) + + def in_ports(self, node: Node) -> Collection[InPort]: + return self.nodes[node].in_ports + + def out_ports(self, node: Node) -> Collection[OutPort]: + return self.nodes[node].out_ports + + def to_serial(self) -> SerialHugr: + return SerialHugr( + version="v1", + # non contiguous indices will be erased + nodes=[node.weight for _, node in sorted(self.nodes.items())], + edges=[ + ((src.node, src.offset), (dst.node, dst.offset)) + for src, dst in self.links.items() + ], + ) + + @classmethod + def from_serial(cls, serial: SerialHugr) -> "Hugr": + raise NotImplementedError + + +@dataclass() +class Dfg(Hugr): + input_node: Node + output_node: Node + + def __init__(self, input_types: Sequence[Type]) -> None: + self.root = Node(0) + + def inputs(self) -> list[OutPort]: + return [] + + def add_op(self, op: DataflowOp, ports: Iterable[ToPort]) -> Node: + node = Node(len(self.nodes)) + # self.nodes[node] = NodeData(op, list(in_ports), []) + return node + + def set_outputs(self, ports: Iterable[ToPort]) -> None: + pass + + +if __name__ == "__main__": + h = Dfg([Type(Qubit())] * 2) From 144487c343359a7edf51b7367a8bd0272dba7537 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Thu, 16 May 2024 11:02:08 +0100 Subject: [PATCH 02/57] some light stable indices --- hugr-py/src/hugr/hugr.py | 65 +++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 0c6135429..2ae94746d 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -1,10 +1,10 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field -from collections.abc import Collection, MutableMapping, Hashable +from collections.abc import Collection, MutableMapping, Hashable, Mapping from typing import Iterable, Sequence, Protocol, Generic, TypeVar from hugr.serialization.serial_hugr import SerialHugr -from hugr.serialization.ops import OpType as Op, DataflowOp +from hugr.serialization.ops import OpType as Op from hugr.serialization.tys import Type, Qubit @@ -92,32 +92,63 @@ class NodeData: @dataclass(init=False) -class Hugr: +class Hugr(Mapping): root: Node - nodes: dict[Node, NodeData] + nodes: list[NodeData | None] links: BiMap[OutPort, InPort] + _free_nodes: list[Node] = field(default_factory=list) + + def __getitem__(self, key: Node) -> NodeData: + try: + n = self.nodes[key.idx] + except IndexError: + n = None + if n is None: + raise KeyError(key) + return n + + def __iter__(self): + return iter(self.nodes) + + def __len__(self) -> int: + return len(self.nodes) - len(self._free_nodes) def add_node(self, op: Op) -> Node: - node = Node(len(self.nodes)) - self.nodes[node] = NodeData(op, set(), set()) + # TODO add in_ports and out_ports + node_data = NodeData(op, set(), set()) + + if self._free_nodes: + node = self._free_nodes.pop() + self.nodes[node.idx] = node_data + else: + node = Node(len(self.nodes)) + self.nodes.append(node_data) return node + def delete_node(self, node: Node) -> None: + for in_port in self[node].in_ports: + self.links.delete_right(in_port) + for out_port in self[node].out_ports: + self.links.delete_left(out_port) + self.nodes[node.idx] = None + self._free_nodes.append(node) + def add_link(self, src: OutPort, dst: InPort) -> None: self.links.insert_left(src, dst) - self.nodes[dst.node].in_ports.add(dst) - self.nodes[src.node].out_ports.add(src) + self[dst.node].in_ports.add(dst) + self[src.node].out_ports.add(src) def in_ports(self, node: Node) -> Collection[InPort]: - return self.nodes[node].in_ports + return self[node].in_ports def out_ports(self, node: Node) -> Collection[OutPort]: - return self.nodes[node].out_ports + return self[node].out_ports def to_serial(self) -> SerialHugr: return SerialHugr( version="v1", # non contiguous indices will be erased - nodes=[node.weight for _, node in sorted(self.nodes.items())], + nodes=[node.weight for node in self.nodes if node is not None], edges=[ ((src.node, src.offset), (dst.node, dst.offset)) for src, dst in self.links.items() @@ -130,7 +161,8 @@ def from_serial(cls, serial: SerialHugr) -> "Hugr": @dataclass() -class Dfg(Hugr): +class Dfg: + hugr: Hugr input_node: Node output_node: Node @@ -140,10 +172,9 @@ def __init__(self, input_types: Sequence[Type]) -> None: def inputs(self) -> list[OutPort]: return [] - def add_op(self, op: DataflowOp, ports: Iterable[ToPort]) -> Node: - node = Node(len(self.nodes)) - # self.nodes[node] = NodeData(op, list(in_ports), []) - return node + def add_op(self, op: Op, ports: Iterable[ToPort]) -> Node: + # TODO wire up ports + return self.hugr.add_node(op) def set_outputs(self, ports: Iterable[ToPort]) -> None: pass From 09060da7881394aa499c84d5f6dbd12e8073416c Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Thu, 16 May 2024 11:08:47 +0100 Subject: [PATCH 03/57] simpler node data --- hugr-py/src/hugr/hugr.py | 52 ++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 2ae94746d..2309c5b58 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -55,21 +55,9 @@ def delete_right(self, key: R) -> None: del self.bck[key] -class ToPort(Protocol): - def to_port(self) -> "OutPort": ... - - -@dataclass(frozen=True, eq=True, order=True) -class Node(ToPort): - idx: int - - def to_port(self) -> "OutPort": - return OutPort(self, 0) - - @dataclass(frozen=True, eq=True, order=True) class Port: - node: Node + node: "Node" offset: int @@ -78,17 +66,35 @@ class InPort(Port): pass +class ToPort(Protocol): + def to_port(self) -> "OutPort": ... + + @dataclass(frozen=True, eq=True, order=True) class OutPort(Port, ToPort): def to_port(self) -> "OutPort": return self +@dataclass(frozen=True, eq=True, order=True) +class Node(ToPort): + idx: int + + def to_port(self) -> "OutPort": + return OutPort(self, 0) + + def in_port(self, offset: int) -> InPort: + return InPort(self, offset) + + def out_port(self, offset: int) -> OutPort: + return OutPort(self, offset) + + @dataclass() class NodeData: weight: Op - in_ports: set[InPort] - out_ports: set[OutPort] + _in_ports: set[int] + _out_ports: set[int] @dataclass(init=False) @@ -126,23 +132,23 @@ def add_node(self, op: Op) -> Node: return node def delete_node(self, node: Node) -> None: - for in_port in self[node].in_ports: - self.links.delete_right(in_port) - for out_port in self[node].out_ports: - self.links.delete_left(out_port) + for offset in self[node]._in_ports: + self.links.delete_right(node.in_port(offset)) + for offset in self[node]._out_ports: + self.links.delete_left(node.out_port(offset)) self.nodes[node.idx] = None self._free_nodes.append(node) def add_link(self, src: OutPort, dst: InPort) -> None: self.links.insert_left(src, dst) - self[dst.node].in_ports.add(dst) - self[src.node].out_ports.add(src) + self[dst.node]._in_ports.add(dst.offset) + self[src.node]._out_ports.add(src.offset) def in_ports(self, node: Node) -> Collection[InPort]: - return self[node].in_ports + return [node.in_port(o) for o in self[node]._in_ports] def out_ports(self, node: Node) -> Collection[OutPort]: - return self[node].out_ports + return [node.out_port(o) for o in self[node]._out_ports] def to_serial(self) -> SerialHugr: return SerialHugr( From b3027bfab1278fc87fecf9d566167c1682eaa1ec Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Thu, 16 May 2024 11:25:05 +0100 Subject: [PATCH 04/57] insert hugr --- hugr-py/src/hugr/hugr.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 2309c5b58..694e7b6e0 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field -from collections.abc import Collection, MutableMapping, Hashable, Mapping +from collections.abc import Collection, ItemsView, MutableMapping, Hashable, Mapping from typing import Iterable, Sequence, Protocol, Generic, TypeVar from hugr.serialization.serial_hugr import SerialHugr @@ -32,6 +32,9 @@ def __iter__(self): def __len__(self) -> int: return len(self.fwd) + def items(self) -> ItemsView[L, R]: + return self.fwd.items() + def get_left(self, key: R) -> L | None: return self.bck.get(key) @@ -83,10 +86,10 @@ class Node(ToPort): def to_port(self) -> "OutPort": return OutPort(self, 0) - def in_port(self, offset: int) -> InPort: + def inp(self, offset: int) -> InPort: return InPort(self, offset) - def out_port(self, offset: int) -> OutPort: + def out(self, offset: int) -> OutPort: return OutPort(self, offset) @@ -101,7 +104,7 @@ class NodeData: class Hugr(Mapping): root: Node nodes: list[NodeData | None] - links: BiMap[OutPort, InPort] + _links: BiMap[OutPort, InPort] _free_nodes: list[Node] = field(default_factory=list) def __getitem__(self, key: Node) -> NodeData: @@ -133,22 +136,33 @@ def add_node(self, op: Op) -> Node: def delete_node(self, node: Node) -> None: for offset in self[node]._in_ports: - self.links.delete_right(node.in_port(offset)) + self._links.delete_right(node.inp(offset)) for offset in self[node]._out_ports: - self.links.delete_left(node.out_port(offset)) + self._links.delete_left(node.out(offset)) self.nodes[node.idx] = None self._free_nodes.append(node) def add_link(self, src: OutPort, dst: InPort) -> None: - self.links.insert_left(src, dst) + self._links.insert_left(src, dst) self[dst.node]._in_ports.add(dst.offset) self[src.node]._out_ports.add(src.offset) def in_ports(self, node: Node) -> Collection[InPort]: - return [node.in_port(o) for o in self[node]._in_ports] + return [node.inp(o) for o in self[node]._in_ports] def out_ports(self, node: Node) -> Collection[OutPort]: - return [node.out_port(o) for o in self[node]._out_ports] + return [node.out(o) for o in self[node]._out_ports] + + def insert_hugr(self, hugr: "Hugr") -> dict[Node, Node]: + mapping: dict[Node, Node] = {} + for idx, node_data in enumerate(self.nodes): + if node_data is not None: + mapping[Node(idx)] = self.add_node(node_data.weight) + for src, dst in hugr._links.items(): + self.add_link( + mapping[src.node].out(src.offset), mapping[dst.node].inp(dst.offset) + ) + return mapping def to_serial(self) -> SerialHugr: return SerialHugr( @@ -156,8 +170,8 @@ def to_serial(self) -> SerialHugr: # non contiguous indices will be erased nodes=[node.weight for node in self.nodes if node is not None], edges=[ - ((src.node, src.offset), (dst.node, dst.offset)) - for src, dst in self.links.items() + ((src.node.idx, src.offset), (dst.node.idx, dst.offset)) + for src, dst in self._links.items() ], ) From 60b1cc5a3f6b19af3e53a6252095e647b69bbc98 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Thu, 16 May 2024 15:11:18 +0100 Subject: [PATCH 05/57] parent tracking in nodedata --- hugr-py/src/hugr/hugr.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 694e7b6e0..265bfbde2 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -4,7 +4,7 @@ from typing import Iterable, Sequence, Protocol, Generic, TypeVar from hugr.serialization.serial_hugr import SerialHugr -from hugr.serialization.ops import OpType as Op +from hugr.serialization.ops import NodeID, OpType as SerialOp, Module from hugr.serialization.tys import Type, Qubit @@ -93,9 +93,22 @@ def out(self, offset: int) -> OutPort: return OutPort(self, offset) +class Op(Protocol): + def to_serial(self, parent: NodeID) -> SerialOp: ... + + +@dataclass(init=False) +class DummyOp(Op): + input_extensions: set[str] | None = None + + def to_serial(self, parent: NodeID) -> SerialOp: + return SerialOp(root=Module(parent=-1)) + + @dataclass() class NodeData: - weight: Op + op: Op + parent: Node | None _in_ports: set[int] _out_ports: set[int] @@ -122,9 +135,10 @@ def __iter__(self): def __len__(self) -> int: return len(self.nodes) - len(self._free_nodes) - def add_node(self, op: Op) -> Node: + def add_node(self, op: Op, parent: Node | None = None) -> Node: + parent = parent or self.root # TODO add in_ports and out_ports - node_data = NodeData(op, set(), set()) + node_data = NodeData(op, parent, set(), set()) if self._free_nodes: node = self._free_nodes.pop() @@ -157,7 +171,7 @@ def insert_hugr(self, hugr: "Hugr") -> dict[Node, Node]: mapping: dict[Node, Node] = {} for idx, node_data in enumerate(self.nodes): if node_data is not None: - mapping[Node(idx)] = self.add_node(node_data.weight) + mapping[Node(idx)] = self.add_node(node_data.op) for src, dst in hugr._links.items(): self.add_link( mapping[src.node].out(src.offset), mapping[dst.node].inp(dst.offset) @@ -165,10 +179,14 @@ def insert_hugr(self, hugr: "Hugr") -> dict[Node, Node]: return mapping def to_serial(self) -> SerialHugr: + node_it = (node for node in self.nodes if node is not None) return SerialHugr( version="v1", # non contiguous indices will be erased - nodes=[node.weight for node in self.nodes if node is not None], + nodes=[ + node.op.to_serial(node.parent.idx if node.parent else idx) + for idx, node in enumerate(node_it) + ], edges=[ ((src.node.idx, src.offset), (dst.node.idx, dst.offset)) for src, dst in self._links.items() From 62ffaffce0fe4500bdddda39c2c9a13f2d3daae0 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Thu, 16 May 2024 17:49:11 +0100 Subject: [PATCH 06/57] flesh out example --- hugr-py/src/hugr/hugr.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 265bfbde2..c30b9a7e3 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -167,11 +167,11 @@ def in_ports(self, node: Node) -> Collection[InPort]: def out_ports(self, node: Node) -> Collection[OutPort]: return [node.out(o) for o in self[node]._out_ports] - def insert_hugr(self, hugr: "Hugr") -> dict[Node, Node]: + def insert_hugr(self, hugr: "Hugr", parent: Node | None = None) -> dict[Node, Node]: mapping: dict[Node, Node] = {} for idx, node_data in enumerate(self.nodes): if node_data is not None: - mapping[Node(idx)] = self.add_node(node_data.op) + mapping[Node(idx)] = self.add_node(node_data.op, parent) for src, dst in hugr._links.items(): self.add_link( mapping[src.node].out(src.offset), mapping[dst.node].inp(dst.offset) @@ -214,9 +214,28 @@ def add_op(self, op: Op, ports: Iterable[ToPort]) -> Node: # TODO wire up ports return self.hugr.add_node(op) + def insert_nested(self, dfg: "Dfg", ports: Iterable[ToPort]) -> Node: + mapping = self.hugr.insert_hugr(dfg.hugr, self.hugr.root) + # TODO wire up ports + return mapping[dfg.hugr.root] + + def add_nested(self, ports: Iterable[ToPort]) -> "Dfg": + dfg = Dfg.__new__(Dfg) + dfg.hugr = self.hugr + # I/O nodes + + return dfg + def set_outputs(self, ports: Iterable[ToPort]) -> None: pass if __name__ == "__main__": h = Dfg([Type(Qubit())] * 2) + + a, b = h.inputs() + x = h.add_op(DummyOp(), [a, b]) + + y = h.add_op(DummyOp(), [x, x]) + + h.set_outputs([y]) From 38c0ba9e43a67f76483da740d6b24ebc2319df01 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Mon, 20 May 2024 11:38:05 +0100 Subject: [PATCH 07/57] add delete_link --- hugr-py/src/hugr/hugr.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index c30b9a7e3..61a181315 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -161,6 +161,11 @@ def add_link(self, src: OutPort, dst: InPort) -> None: self[dst.node]._in_ports.add(dst.offset) self[src.node]._out_ports.add(src.offset) + def delete_link(self, src: OutPort, dst: InPort) -> None: + self._links.delete_left(src) + self[dst.node]._in_ports.remove(dst.offset) + self[src.node]._out_ports.remove(src.offset) + def in_ports(self, node: Node) -> Collection[InPort]: return [node.inp(o) for o in self[node]._in_ports] From 7ebff33f31a770eb4ccf117a2941981f233eb87f Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Mon, 20 May 2024 14:55:01 +0100 Subject: [PATCH 08/57] simple identity test --- hugr-py/src/hugr/hugr.py | 83 +++++++++++++---------- hugr-py/tests/serialization/test_basic.py | 23 +++++++ 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 61a181315..5c83f9605 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -4,18 +4,19 @@ from typing import Iterable, Sequence, Protocol, Generic, TypeVar from hugr.serialization.serial_hugr import SerialHugr -from hugr.serialization.ops import NodeID, OpType as SerialOp, Module -from hugr.serialization.tys import Type, Qubit +from hugr.serialization.ops import BaseOp, NodeID, OpType as SerialOp +import hugr.serialization.ops as sops +from hugr.serialization.tys import Type L = TypeVar("L", bound=Hashable) R = TypeVar("R", bound=Hashable) -@dataclass(init=False) +@dataclass() class BiMap(MutableMapping, Generic[L, R]): - fwd: dict[L, R] - bck: dict[R, L] + fwd: dict[L, R] = field(default_factory=dict) + bck: dict[R, L] = field(default_factory=dict) def __getitem__(self, key: L) -> R: return self.fwd[key] @@ -97,12 +98,16 @@ class Op(Protocol): def to_serial(self, parent: NodeID) -> SerialOp: ... -@dataclass(init=False) -class DummyOp(Op): - input_extensions: set[str] | None = None +T = TypeVar("T", bound=BaseOp) + + +@dataclass() +class DummyOp(Op, Generic[T]): + _serial_op: T def to_serial(self, parent: NodeID) -> SerialOp: - return SerialOp(root=Module(parent=-1)) + self._serial_op.parent = parent + return SerialOp(root=self._serial_op) # type: ignore @dataclass() @@ -113,16 +118,21 @@ class NodeData: _out_ports: set[int] -@dataclass(init=False) class Hugr(Mapping): root: Node - nodes: list[NodeData | None] + _nodes: list[NodeData | None] _links: BiMap[OutPort, InPort] - _free_nodes: list[Node] = field(default_factory=list) + _free_nodes: list[Node] + + def __init__(self, root_op: Op) -> None: + self.root = Node(0) + self._nodes = [NodeData(root_op, None, set(), set())] + self._links = BiMap() + self._free_nodes = [] def __getitem__(self, key: Node) -> NodeData: try: - n = self.nodes[key.idx] + n = self._nodes[key.idx] except IndexError: n = None if n is None: @@ -130,10 +140,10 @@ def __getitem__(self, key: Node) -> NodeData: return n def __iter__(self): - return iter(self.nodes) + return iter(self._nodes) def __len__(self) -> int: - return len(self.nodes) - len(self._free_nodes) + return len(self._nodes) - len(self._free_nodes) def add_node(self, op: Op, parent: Node | None = None) -> Node: parent = parent or self.root @@ -142,10 +152,10 @@ def add_node(self, op: Op, parent: Node | None = None) -> Node: if self._free_nodes: node = self._free_nodes.pop() - self.nodes[node.idx] = node_data + self._nodes[node.idx] = node_data else: - node = Node(len(self.nodes)) - self.nodes.append(node_data) + node = Node(len(self._nodes)) + self._nodes.append(node_data) return node def delete_node(self, node: Node) -> None: @@ -153,7 +163,7 @@ def delete_node(self, node: Node) -> None: self._links.delete_right(node.inp(offset)) for offset in self[node]._out_ports: self._links.delete_left(node.out(offset)) - self.nodes[node.idx] = None + self._nodes[node.idx] = None self._free_nodes.append(node) def add_link(self, src: OutPort, dst: InPort) -> None: @@ -174,7 +184,7 @@ def out_ports(self, node: Node) -> Collection[OutPort]: def insert_hugr(self, hugr: "Hugr", parent: Node | None = None) -> dict[Node, Node]: mapping: dict[Node, Node] = {} - for idx, node_data in enumerate(self.nodes): + for idx, node_data in enumerate(self._nodes): if node_data is not None: mapping[Node(idx)] = self.add_node(node_data.op, parent) for src, dst in hugr._links.items(): @@ -184,7 +194,7 @@ def insert_hugr(self, hugr: "Hugr", parent: Node | None = None) -> dict[Node, No return mapping def to_serial(self) -> SerialHugr: - node_it = (node for node in self.nodes if node is not None) + node_it = (node for node in self._nodes if node is not None) return SerialHugr( version="v1", # non contiguous indices will be erased @@ -208,12 +218,25 @@ class Dfg: hugr: Hugr input_node: Node output_node: Node + _n_input: int def __init__(self, input_types: Sequence[Type]) -> None: - self.root = Node(0) + self._n_input = len(input_types) + input_types = list(input_types) + root_op = DummyOp(sops.DFG(parent=-1)) + root_op._serial_op.signature.input = input_types + # TODO don't assume endo output + root_op._serial_op.signature.output = input_types + self.hugr = Hugr(root_op) + self.input_node = self.hugr.add_node( + DummyOp(sops.Input(parent=0, types=input_types)) + ) + self.output_node = self.hugr.add_node( + DummyOp(sops.Output(parent=0, types=input_types)) + ) def inputs(self) -> list[OutPort]: - return [] + return [self.input_node.out(i) for i in range(self._n_input)] def add_op(self, op: Op, ports: Iterable[ToPort]) -> Node: # TODO wire up ports @@ -232,15 +255,5 @@ def add_nested(self, ports: Iterable[ToPort]) -> "Dfg": return dfg def set_outputs(self, ports: Iterable[ToPort]) -> None: - pass - - -if __name__ == "__main__": - h = Dfg([Type(Qubit())] * 2) - - a, b = h.inputs() - x = h.add_op(DummyOp(), [a, b]) - - y = h.add_op(DummyOp(), [x, x]) - - h.set_outputs([y]) + for i, p in enumerate(ports): + self.hugr.add_link(p.to_port(), self.output_node.inp(i)) diff --git a/hugr-py/tests/serialization/test_basic.py b/hugr-py/tests/serialization/test_basic.py index 5c3b41ace..580bbc6bc 100644 --- a/hugr-py/tests/serialization/test_basic.py +++ b/hugr-py/tests/serialization/test_basic.py @@ -1,4 +1,6 @@ from hugr.serialization import SerialHugr +from hugr.hugr import Dfg, Type, Hugr +from hugr.serialization.tys import Qubit def test_empty(): @@ -10,3 +12,24 @@ def test_empty(): "metadata": None, "encoder": None, } + + +def _validate(h: Hugr): + import subprocess + import tempfile + + with tempfile.NamedTemporaryFile("w") as f: + f.write(h.to_serial().to_json()) + f.flush() + # TODO point to built hugr binary + subprocess.run(["cargo", "run", f.name], check=True) + + +def test_simple_id(): + h = Dfg([Type(Qubit())] * 2) + + a, b = h.inputs() + + h.set_outputs([a, b]) + + _validate(h.hugr) From 4c2951333577de33e34bdf39d9493327658c52fa Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Mon, 20 May 2024 15:33:02 +0100 Subject: [PATCH 09/57] refactor out bimap and add tests --- hugr-py/src/hugr/hugr.py | 55 ++----------------------------------- hugr-py/src/hugr/utils.py | 53 +++++++++++++++++++++++++++++++++++ hugr-py/tests/test_bimap.py | 52 +++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 52 deletions(-) create mode 100644 hugr-py/src/hugr/utils.py create mode 100644 hugr-py/tests/test_bimap.py diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 5c83f9605..53f3869e1 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -1,62 +1,13 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass -from collections.abc import Collection, ItemsView, MutableMapping, Hashable, Mapping +from collections.abc import Collection, Mapping from typing import Iterable, Sequence, Protocol, Generic, TypeVar from hugr.serialization.serial_hugr import SerialHugr from hugr.serialization.ops import BaseOp, NodeID, OpType as SerialOp import hugr.serialization.ops as sops from hugr.serialization.tys import Type - - -L = TypeVar("L", bound=Hashable) -R = TypeVar("R", bound=Hashable) - - -@dataclass() -class BiMap(MutableMapping, Generic[L, R]): - fwd: dict[L, R] = field(default_factory=dict) - bck: dict[R, L] = field(default_factory=dict) - - def __getitem__(self, key: L) -> R: - return self.fwd[key] - - def __setitem__(self, key: L, value: R) -> None: - self.insert_left(key, value) - - def __delitem__(self, key: L) -> None: - self.delete_left(key) - - def __iter__(self): - return iter(self.fwd) - - def __len__(self) -> int: - return len(self.fwd) - - def items(self) -> ItemsView[L, R]: - return self.fwd.items() - - def get_left(self, key: R) -> L | None: - return self.bck.get(key) - - def get_right(self, key: L) -> R | None: - return self.fwd.get(key) - - def insert_left(self, key: L, value: R) -> None: - self.fwd[key] = value - self.bck[value] = key - - def insert_right(self, key: R, value: L) -> None: - self.bck[key] = value - self.fwd[value] = key - - def delete_left(self, key: L) -> None: - del self.bck[self.fwd[key]] - del self.fwd[key] - - def delete_right(self, key: R) -> None: - del self.fwd[self.bck[key]] - del self.bck[key] +from hugr.utils import BiMap @dataclass(frozen=True, eq=True, order=True) diff --git a/hugr-py/src/hugr/utils.py b/hugr-py/src/hugr/utils.py new file mode 100644 index 000000000..f7d9fef25 --- /dev/null +++ b/hugr-py/src/hugr/utils.py @@ -0,0 +1,53 @@ +from collections.abc import Hashable, ItemsView, MutableMapping +from dataclasses import dataclass, field +from typing import Generic, TypeVar + + +L = TypeVar("L", bound=Hashable) +R = TypeVar("R", bound=Hashable) + + +@dataclass() +class BiMap(MutableMapping, Generic[L, R]): + fwd: dict[L, R] = field(default_factory=dict) + bck: dict[R, L] = field(default_factory=dict) + + def __getitem__(self, key: L) -> R: + return self.fwd[key] + + def __setitem__(self, key: L, value: R) -> None: + self.insert_left(key, value) + + def __delitem__(self, key: L) -> None: + self.delete_left(key) + + def __iter__(self): + return iter(self.fwd) + + def __len__(self) -> int: + return len(self.fwd) + + def items(self) -> ItemsView[L, R]: + return self.fwd.items() + + def get_left(self, key: R) -> L | None: + return self.bck.get(key) + + def get_right(self, key: L) -> R | None: + return self.fwd.get(key) + + def insert_left(self, key: L, value: R) -> None: + self.fwd[key] = value + self.bck[value] = key + + def insert_right(self, key: R, value: L) -> None: + self.bck[key] = value + self.fwd[value] = key + + def delete_left(self, key: L) -> None: + del self.bck[self.fwd[key]] + del self.fwd[key] + + def delete_right(self, key: R) -> None: + del self.fwd[self.bck[key]] + del self.bck[key] diff --git a/hugr-py/tests/test_bimap.py b/hugr-py/tests/test_bimap.py new file mode 100644 index 000000000..85177e5bd --- /dev/null +++ b/hugr-py/tests/test_bimap.py @@ -0,0 +1,52 @@ +from hugr.utils import BiMap + + +def test_insert_left(): + bimap: BiMap[str, int] = BiMap() + bimap.insert_left("a", 1) + assert bimap["a"] == 1 + assert bimap.get_left(1) == "a" + + +def test_insert_right(): + bimap: BiMap[str, int] = BiMap() + bimap.insert_right(1, "a") + assert bimap["a"] == 1 + assert bimap.get_left(1) == "a" + + +def test_delete_left(): + bimap: BiMap[str, int] = BiMap() + bimap.insert_left("a", 1) + del bimap["a"] + assert bimap.get_right("a") is None + assert bimap.get_left(1) is None + + +def test_delete_right(): + bimap: BiMap[str, int] = BiMap() + bimap.insert_right(1, "a") + bimap.delete_right(1) + assert bimap.get_right("a") is None + assert bimap.get_left(1) is None + + +def test_iter(): + bimap: BiMap[str, int] = BiMap() + bimap.insert_left("a", 1) + bimap.insert_left("b", 2) + bimap.insert_left("c", 3) + assert set(bimap) == {"a", "b", "c"} + assert list(bimap.items()) == [("a", 1), ("b", 2), ("c", 3)] + + +def test_len(): + bimap: BiMap[str, int] = BiMap() + assert len(bimap) == 0 + bimap.insert_left("a", 1) + assert len(bimap) == 1 + bimap.insert_left("b", 2) + assert len(bimap) == 2 + + bimap.delete_left("a") + assert len(bimap) == 1 From 6469d76ff9b441c01e15dd810fa14413297f2602 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Mon, 20 May 2024 17:56:15 +0100 Subject: [PATCH 10/57] tet annotations --- hugr-py/tests/test_bimap.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hugr-py/tests/test_bimap.py b/hugr-py/tests/test_bimap.py index 85177e5bd..bd17835eb 100644 --- a/hugr-py/tests/test_bimap.py +++ b/hugr-py/tests/test_bimap.py @@ -1,21 +1,21 @@ from hugr.utils import BiMap -def test_insert_left(): +def test_insert_left() -> None: bimap: BiMap[str, int] = BiMap() bimap.insert_left("a", 1) assert bimap["a"] == 1 assert bimap.get_left(1) == "a" -def test_insert_right(): +def test_insert_right() -> None: bimap: BiMap[str, int] = BiMap() bimap.insert_right(1, "a") assert bimap["a"] == 1 assert bimap.get_left(1) == "a" -def test_delete_left(): +def test_delete_left() -> None: bimap: BiMap[str, int] = BiMap() bimap.insert_left("a", 1) del bimap["a"] @@ -23,7 +23,7 @@ def test_delete_left(): assert bimap.get_left(1) is None -def test_delete_right(): +def test_delete_right() -> None: bimap: BiMap[str, int] = BiMap() bimap.insert_right(1, "a") bimap.delete_right(1) @@ -31,7 +31,7 @@ def test_delete_right(): assert bimap.get_left(1) is None -def test_iter(): +def test_iter() -> None: bimap: BiMap[str, int] = BiMap() bimap.insert_left("a", 1) bimap.insert_left("b", 2) @@ -40,7 +40,7 @@ def test_iter(): assert list(bimap.items()) == [("a", 1), ("b", 2), ("c", 3)] -def test_len(): +def test_len() -> None: bimap: BiMap[str, int] = BiMap() assert len(bimap) == 0 bimap.insert_left("a", 1) From 915a015f992a708e5f049cfc310f069623d928e1 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Mon, 20 May 2024 17:56:56 +0100 Subject: [PATCH 11/57] [wip] get types from nodes --- hugr-py/src/hugr/hugr.py | 106 +++++++++++++++++++++++++++------------ 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 53f3869e1..ae4321315 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -1,10 +1,10 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from collections.abc import Collection, Mapping from typing import Iterable, Sequence, Protocol, Generic, TypeVar from hugr.serialization.serial_hugr import SerialHugr -from hugr.serialization.ops import BaseOp, NodeID, OpType as SerialOp +from hugr.serialization.ops import BaseOp, OpType as SerialOp import hugr.serialization.ops as sops from hugr.serialization.tys import Type from hugr.utils import BiMap @@ -46,7 +46,7 @@ def out(self, offset: int) -> OutPort: class Op(Protocol): - def to_serial(self, parent: NodeID) -> SerialOp: ... + def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: ... T = TypeVar("T", bound=BaseOp) @@ -56,8 +56,7 @@ def to_serial(self, parent: NodeID) -> SerialOp: ... class DummyOp(Op, Generic[T]): _serial_op: T - def to_serial(self, parent: NodeID) -> SerialOp: - self._serial_op.parent = parent + def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: return SerialOp(root=self._serial_op) # type: ignore @@ -65,8 +64,30 @@ def to_serial(self, parent: NodeID) -> SerialOp: class NodeData: op: Op parent: Node | None - _in_ports: set[int] - _out_ports: set[int] + _in_ports: list[Type | None] = field(default_factory=list) + _out_ports: list[Type | None] = field(default_factory=list) + # TODO children field? + + def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: + o = self.op.to_serial(node, hugr) + o.root.parent = self.parent.idx if self.parent else node.idx + if all_not_none(self._in_ports) and all_not_none(self._out_ports): + o.root.insert_port_types(self._in_ports, self._out_ports) + + return o + + +L = TypeVar("L") + + +def _insert_or_extend(lst: list[L | None], idx: int, value: L | None) -> None: + if idx >= len(lst): + lst.extend([None] * (idx - len(lst) + 1)) + lst[idx] = value + + +def all_not_none(lst: list[L | None]) -> bool: + return all(i is not None for i in lst) class Hugr(Mapping): @@ -77,7 +98,7 @@ class Hugr(Mapping): def __init__(self, root_op: Op) -> None: self.root = Node(0) - self._nodes = [NodeData(root_op, None, set(), set())] + self._nodes = [NodeData(root_op, None)] self._links = BiMap() self._free_nodes = [] @@ -96,10 +117,18 @@ def __iter__(self): def __len__(self) -> int: return len(self._nodes) - len(self._free_nodes) - def add_node(self, op: Op, parent: Node | None = None) -> Node: + def add_node( + self, + op: Op, + parent: Node | None = None, + input_types: Sequence[Type | None] | None = None, + output_types: Sequence[Type | None] | None = None, + ) -> Node: + _input_types = list(input_types or []) + _output_types = list(output_types or []) parent = parent or self.root # TODO add in_ports and out_ports - node_data = NodeData(op, parent, set(), set()) + node_data = NodeData(op, parent, _input_types, _output_types) if self._free_nodes: node = self._free_nodes.pop() @@ -110,28 +139,43 @@ def add_node(self, op: Op, parent: Node | None = None) -> Node: return node def delete_node(self, node: Node) -> None: - for offset in self[node]._in_ports: + for offset in range(self.num_in_ports(node)): self._links.delete_right(node.inp(offset)) - for offset in self[node]._out_ports: + for offset in range(self.num_out_ports(node)): self._links.delete_left(node.out(offset)) self._nodes[node.idx] = None self._free_nodes.append(node) - def add_link(self, src: OutPort, dst: InPort) -> None: + def add_link(self, src: OutPort, dst: InPort, ty: Type | None = None) -> None: self._links.insert_left(src, dst) - self[dst.node]._in_ports.add(dst.offset) - self[src.node]._out_ports.add(src.offset) + if ty is None: + ty = self.port_type(src) + if ty is None: + ty = self.port_type(dst) + _insert_or_extend(self[dst.node]._in_ports, dst.offset, ty) + _insert_or_extend(self[src.node]._out_ports, src.offset, ty) def delete_link(self, src: OutPort, dst: InPort) -> None: self._links.delete_left(src) - self[dst.node]._in_ports.remove(dst.offset) - self[src.node]._out_ports.remove(src.offset) + + def num_in_ports(self, node: Node) -> int: + return len(self[node]._in_ports) + + def num_out_ports(self, node: Node) -> int: + return len(self[node]._out_ports) def in_ports(self, node: Node) -> Collection[InPort]: - return [node.inp(o) for o in self[node]._in_ports] + return [node.inp(o) for o in range(self.num_in_ports(node))] def out_ports(self, node: Node) -> Collection[OutPort]: - return [node.out(o) for o in self[node]._out_ports] + return [node.out(o) for o in range(self.num_out_ports(node))] + + def port_type(self, port: InPort | OutPort) -> Type | None: + match port: + case InPort(node, offset): + return self[node]._in_ports[offset] + case OutPort(node, offset): + return self[node]._out_ports[offset] def insert_hugr(self, hugr: "Hugr", parent: Node | None = None) -> dict[Node, Node]: mapping: dict[Node, Node] = {} @@ -149,10 +193,7 @@ def to_serial(self) -> SerialHugr: return SerialHugr( version="v1", # non contiguous indices will be erased - nodes=[ - node.op.to_serial(node.parent.idx if node.parent else idx) - for idx, node in enumerate(node_it) - ], + nodes=[node.to_serial(Node(idx), self) for idx, node in enumerate(node_it)], edges=[ ((src.node.idx, src.offset), (dst.node.idx, dst.offset)) for src, dst in self._links.items() @@ -169,7 +210,6 @@ class Dfg: hugr: Hugr input_node: Node output_node: Node - _n_input: int def __init__(self, input_types: Sequence[Type]) -> None: self._n_input = len(input_types) @@ -177,17 +217,17 @@ def __init__(self, input_types: Sequence[Type]) -> None: root_op = DummyOp(sops.DFG(parent=-1)) root_op._serial_op.signature.input = input_types # TODO don't assume endo output - root_op._serial_op.signature.output = input_types self.hugr = Hugr(root_op) self.input_node = self.hugr.add_node( - DummyOp(sops.Input(parent=0, types=input_types)) - ) - self.output_node = self.hugr.add_node( - DummyOp(sops.Output(parent=0, types=input_types)) + DummyOp(sops.Input(parent=0)), output_types=input_types ) + self.output_node = self.hugr.add_node(DummyOp(sops.Output(parent=0, types=[]))) def inputs(self) -> list[OutPort]: - return [self.input_node.out(i) for i in range(self._n_input)] + return [ + self.input_node.out(i) + for i in range(self.hugr.num_out_ports(self.input_node)) + ] def add_op(self, op: Op, ports: Iterable[ToPort]) -> Node: # TODO wire up ports @@ -207,4 +247,8 @@ def add_nested(self, ports: Iterable[ToPort]) -> "Dfg": def set_outputs(self, ports: Iterable[ToPort]) -> None: for i, p in enumerate(ports): - self.hugr.add_link(p.to_port(), self.output_node.inp(i)) + src = p.to_port() + self.hugr.add_link(src, self.output_node.inp(i)) + ty = self.hugr.port_type(src) + _insert_or_extend(self.hugr[self.output_node]._in_ports, i, ty) + _insert_or_extend(self.hugr[self.hugr.root]._out_ports, i, ty) From fd8cce1e3418a2080efae50364d3fe899fab9e06 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 21 May 2024 11:14:11 +0100 Subject: [PATCH 12/57] remove "type inference" --- hugr-py/src/hugr/hugr.py | 74 ++++++++++++----------- hugr-py/tests/serialization/test_basic.py | 3 +- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index ae4321315..6ad6f63d5 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from collections.abc import Collection, Mapping from typing import Iterable, Sequence, Protocol, Generic, TypeVar @@ -64,15 +64,15 @@ def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: class NodeData: op: Op parent: Node | None - _in_ports: list[Type | None] = field(default_factory=list) - _out_ports: list[Type | None] = field(default_factory=list) + _n_in_ports: int = 0 + _n_out_ports: int = 0 # TODO children field? def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: o = self.op.to_serial(node, hugr) o.root.parent = self.parent.idx if self.parent else node.idx - if all_not_none(self._in_ports) and all_not_none(self._out_ports): - o.root.insert_port_types(self._in_ports, self._out_ports) + # if all_not_none(self._in_ports) and all_not_none(self._out_ports): + # o.root.insert_port_types(self._in_ports, self._out_ports) return o @@ -86,7 +86,7 @@ def _insert_or_extend(lst: list[L | None], idx: int, value: L | None) -> None: lst[idx] = value -def all_not_none(lst: list[L | None]) -> bool: +def _all_not_none(lst: list[L | None]) -> bool: return all(i is not None for i in lst) @@ -121,14 +121,12 @@ def add_node( self, op: Op, parent: Node | None = None, - input_types: Sequence[Type | None] | None = None, - output_types: Sequence[Type | None] | None = None, + n_inputs: int | None = None, + n_outputs: int | None = None, ) -> Node: - _input_types = list(input_types or []) - _output_types = list(output_types or []) parent = parent or self.root # TODO add in_ports and out_ports - node_data = NodeData(op, parent, _input_types, _output_types) + node_data = NodeData(op, parent, n_inputs or 0, n_outputs or 0) if self._free_nodes: node = self._free_nodes.pop() @@ -148,21 +146,23 @@ def delete_node(self, node: Node) -> None: def add_link(self, src: OutPort, dst: InPort, ty: Type | None = None) -> None: self._links.insert_left(src, dst) - if ty is None: - ty = self.port_type(src) - if ty is None: - ty = self.port_type(dst) - _insert_or_extend(self[dst.node]._in_ports, dst.offset, ty) - _insert_or_extend(self[src.node]._out_ports, src.offset, ty) + self[src.node]._n_out_ports = max(self[src.node]._n_out_ports, src.offset + 1) + self[dst.node]._n_in_ports = max(self[dst.node]._n_in_ports, dst.offset + 1) + # if ty is None: + # ty = self.port_type(src) + # if ty is None: + # ty = self.port_type(dst) + # _insert_or_extend(self[dst.node]._in_ports, dst.offset, ty) + # _insert_or_extend(self[src.node]._out_ports, src.offset, ty) def delete_link(self, src: OutPort, dst: InPort) -> None: self._links.delete_left(src) def num_in_ports(self, node: Node) -> int: - return len(self[node]._in_ports) + return self[node]._n_in_ports def num_out_ports(self, node: Node) -> int: - return len(self[node]._out_ports) + return self[node]._n_out_ports def in_ports(self, node: Node) -> Collection[InPort]: return [node.inp(o) for o in range(self.num_in_ports(node))] @@ -170,13 +170,6 @@ def in_ports(self, node: Node) -> Collection[InPort]: def out_ports(self, node: Node) -> Collection[OutPort]: return [node.out(o) for o in range(self.num_out_ports(node))] - def port_type(self, port: InPort | OutPort) -> Type | None: - match port: - case InPort(node, offset): - return self[node]._in_ports[offset] - case OutPort(node, offset): - return self[node]._out_ports[offset] - def insert_hugr(self, hugr: "Hugr", parent: Node | None = None) -> dict[Node, Node]: mapping: dict[Node, Node] = {} for idx, node_data in enumerate(self._nodes): @@ -211,22 +204,36 @@ class Dfg: input_node: Node output_node: Node - def __init__(self, input_types: Sequence[Type]) -> None: - self._n_input = len(input_types) + def __init__( + self, input_types: Sequence[Type], output_types: Sequence[Type] + ) -> None: input_types = list(input_types) + output_types = list(output_types) root_op = DummyOp(sops.DFG(parent=-1)) root_op._serial_op.signature.input = input_types - # TODO don't assume endo output + root_op._serial_op.signature.output = output_types self.hugr = Hugr(root_op) self.input_node = self.hugr.add_node( - DummyOp(sops.Input(parent=0)), output_types=input_types + DummyOp(sops.Input(parent=0, types=input_types)) + ) + self.output_node = self.hugr.add_node( + DummyOp(sops.Output(parent=0, types=output_types)) ) - self.output_node = self.hugr.add_node(DummyOp(sops.Output(parent=0, types=[]))) + + @classmethod + def endo(cls, types: Sequence[Type]) -> "Dfg": + return Dfg(types, types) + + def _input_op(self) -> DummyOp[sops.Input]: + dop = self.hugr[self.input_node].op + assert isinstance(dop, DummyOp) + assert isinstance(dop._serial_op, sops.Input) + return dop def inputs(self) -> list[OutPort]: return [ self.input_node.out(i) - for i in range(self.hugr.num_out_ports(self.input_node)) + for i in range(len(self._input_op()._serial_op.types)) ] def add_op(self, op: Op, ports: Iterable[ToPort]) -> Node: @@ -249,6 +256,3 @@ def set_outputs(self, ports: Iterable[ToPort]) -> None: for i, p in enumerate(ports): src = p.to_port() self.hugr.add_link(src, self.output_node.inp(i)) - ty = self.hugr.port_type(src) - _insert_or_extend(self.hugr[self.output_node]._in_ports, i, ty) - _insert_or_extend(self.hugr[self.hugr.root]._out_ports, i, ty) diff --git a/hugr-py/tests/serialization/test_basic.py b/hugr-py/tests/serialization/test_basic.py index 580bbc6bc..fd7d87ee7 100644 --- a/hugr-py/tests/serialization/test_basic.py +++ b/hugr-py/tests/serialization/test_basic.py @@ -26,7 +26,8 @@ def _validate(h: Hugr): def test_simple_id(): - h = Dfg([Type(Qubit())] * 2) + qb_row = [Type(Qubit())] * 2 + h = Dfg.endo(qb_row) a, b = h.inputs() From 4b5dae067ba4ef393975938df9c80b8a4986999b Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 21 May 2024 11:35:23 +0100 Subject: [PATCH 13/57] handle multiports --- hugr-py/src/hugr/hugr.py | 31 ++++++++++++++++++++++- hugr-py/tests/serialization/test_basic.py | 16 +++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 6ad6f63d5..b49d7c941 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, replace from collections.abc import Collection, Mapping from typing import Iterable, Sequence, Protocol, Generic, TypeVar @@ -7,6 +7,7 @@ from hugr.serialization.ops import BaseOp, OpType as SerialOp import hugr.serialization.ops as sops from hugr.serialization.tys import Type +import hugr.serialization.tys as stys from hugr.utils import BiMap @@ -14,6 +15,7 @@ class Port: node: "Node" offset: int + sub_offset: int = 0 @dataclass(frozen=True, eq=True, order=True) @@ -90,6 +92,21 @@ def _all_not_none(lst: list[L | None]) -> bool: return all(i is not None for i in lst) +P = TypeVar("P", InPort, OutPort) + + +def _unused_sub_offset(port: P, links: BiMap[OutPort, InPort]) -> P: + d: dict[OutPort, InPort] | dict[InPort, OutPort] + match port: + case OutPort(_): + d = links.fwd + case InPort(_): + d = links.bck + while port in d: + port = replace(port, sub_offset=port.sub_offset + 1) + return port + + class Hugr(Mapping): root: Node _nodes: list[NodeData | None] @@ -145,6 +162,10 @@ def delete_node(self, node: Node) -> None: self._free_nodes.append(node) def add_link(self, src: OutPort, dst: InPort, ty: Type | None = None) -> None: + src = _unused_sub_offset(src, self._links) + dst = _unused_sub_offset(dst, self._links) + if self._links.get_left(dst) is not None: + dst = replace(dst, sub_offset=dst.sub_offset + 1) self._links.insert_left(src, dst) self[src.node]._n_out_ports = max(self[src.node]._n_out_ports, src.offset + 1) self[dst.node]._n_in_ports = max(self[dst.node]._n_in_ports, dst.offset + 1) @@ -256,3 +277,11 @@ def set_outputs(self, ports: Iterable[ToPort]) -> None: for i, p in enumerate(ports): src = p.to_port() self.hugr.add_link(src, self.output_node.inp(i)) + + +# ---------------------------------------------- +# --------------- Type ------------------------- +# ---------------------------------------------- + +BOOL_T = Type(stys.SumType(stys.UnitSum(size=2))) +QB_T = Type(stys.Qubit()) diff --git a/hugr-py/tests/serialization/test_basic.py b/hugr-py/tests/serialization/test_basic.py index fd7d87ee7..5291f0355 100644 --- a/hugr-py/tests/serialization/test_basic.py +++ b/hugr-py/tests/serialization/test_basic.py @@ -1,6 +1,5 @@ from hugr.serialization import SerialHugr -from hugr.hugr import Dfg, Type, Hugr -from hugr.serialization.tys import Qubit +from hugr.hugr import Dfg, Hugr, QB_T, BOOL_T def test_empty(): @@ -26,11 +25,20 @@ def _validate(h: Hugr): def test_simple_id(): - qb_row = [Type(Qubit())] * 2 - h = Dfg.endo(qb_row) + h = Dfg.endo([QB_T] * 2) a, b = h.inputs() h.set_outputs([a, b]) _validate(h.hugr) + + +def test_multiport(): + h = Dfg([BOOL_T], [BOOL_T] * 2) + + (a,) = h.inputs() + + h.set_outputs([a, a]) + + _validate(h.hugr) From 76a670059c73e706dc3342f247a41dc6a3f9e46b Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 21 May 2024 12:16:20 +0100 Subject: [PATCH 14/57] test `add_op` --- hugr-py/src/hugr/hugr.py | 21 +++++------- hugr-py/tests/serialization/test_basic.py | 39 +++++++++++++++++++---- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index b49d7c941..1e8483fc6 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -7,7 +7,6 @@ from hugr.serialization.ops import BaseOp, OpType as SerialOp import hugr.serialization.ops as sops from hugr.serialization.tys import Type -import hugr.serialization.tys as stys from hugr.utils import BiMap @@ -258,12 +257,13 @@ def inputs(self) -> list[OutPort]: ] def add_op(self, op: Op, ports: Iterable[ToPort]) -> Node: - # TODO wire up ports - return self.hugr.add_node(op) + new_n = self.hugr.add_node(op) + self._wire_up(new_n, ports) + return new_n def insert_nested(self, dfg: "Dfg", ports: Iterable[ToPort]) -> Node: mapping = self.hugr.insert_hugr(dfg.hugr, self.hugr.root) - # TODO wire up ports + self._wire_up(mapping[dfg.hugr.root], ports) return mapping[dfg.hugr.root] def add_nested(self, ports: Iterable[ToPort]) -> "Dfg": @@ -274,14 +274,9 @@ def add_nested(self, ports: Iterable[ToPort]) -> "Dfg": return dfg def set_outputs(self, ports: Iterable[ToPort]) -> None: + self._wire_up(self.output_node, ports) + + def _wire_up(self, node: Node, ports: Iterable[ToPort]): for i, p in enumerate(ports): src = p.to_port() - self.hugr.add_link(src, self.output_node.inp(i)) - - -# ---------------------------------------------- -# --------------- Type ------------------------- -# ---------------------------------------------- - -BOOL_T = Type(stys.SumType(stys.UnitSum(size=2))) -QB_T = Type(stys.Qubit()) + self.hugr.add_link(src, node.inp(i)) diff --git a/hugr-py/tests/serialization/test_basic.py b/hugr-py/tests/serialization/test_basic.py index 5291f0355..b9865fd3e 100644 --- a/hugr-py/tests/serialization/test_basic.py +++ b/hugr-py/tests/serialization/test_basic.py @@ -1,5 +1,21 @@ from hugr.serialization import SerialHugr -from hugr.hugr import Dfg, Hugr, QB_T, BOOL_T +from hugr.hugr import Dfg, Hugr, DummyOp +import hugr.serialization.tys as stys +import hugr.serialization.ops as sops + +BOOL_T = stys.Type(stys.SumType(stys.UnitSum(size=2))) +QB_T = stys.Type(stys.Qubit()) + + +NOT_OP = DummyOp( + # TODO get from YAML + sops.CustomOp( + parent=-1, + extension="logic", + op_name="Not", + signature=stys.FunctionType(input=[BOOL_T], output=[BOOL_T]), + ) +) def test_empty(): @@ -13,7 +29,7 @@ def test_empty(): } -def _validate(h: Hugr): +def _validate(h: Hugr, mermaid: bool = False): import subprocess import tempfile @@ -21,14 +37,16 @@ def _validate(h: Hugr): f.write(h.to_serial().to_json()) f.flush() # TODO point to built hugr binary - subprocess.run(["cargo", "run", f.name], check=True) + cmd = ["cargo", "run", "--"] + + if mermaid: + cmd.append("--mermaid") + subprocess.run(cmd + [f.name], check=True) def test_simple_id(): h = Dfg.endo([QB_T] * 2) - a, b = h.inputs() - h.set_outputs([a, b]) _validate(h.hugr) @@ -36,9 +54,16 @@ def test_simple_id(): def test_multiport(): h = Dfg([BOOL_T], [BOOL_T] * 2) - (a,) = h.inputs() - h.set_outputs([a, a]) _validate(h.hugr) + + +def test_add_op(): + h = Dfg.endo([BOOL_T]) + (a,) = h.inputs() + nt = h.add_op(NOT_OP, [a]) + h.set_outputs([nt]) + + _validate(h.hugr) From eb738e591a939c076b6bd54abf6ccb563fc6f935 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 21 May 2024 12:27:23 +0100 Subject: [PATCH 15/57] add tuple handling --- hugr-py/src/hugr/hugr.py | 11 +++++++++++ hugr-py/tests/serialization/test_basic.py | 23 ++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 1e8483fc6..b0df0bcff 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -276,6 +276,17 @@ def add_nested(self, ports: Iterable[ToPort]) -> "Dfg": def set_outputs(self, ports: Iterable[ToPort]) -> None: self._wire_up(self.output_node, ports) + def make_tuple(self, ports: Iterable[ToPort], tys: Sequence[Type]) -> Node: + ports = list(ports) + assert len(tys) == len(ports), "Number of types must match number of ports" + return self.add_op(DummyOp(sops.MakeTuple(parent=-1, tys=list(tys))), ports) + + def split_tuple(self, port: ToPort, tys: Sequence[Type]) -> list[OutPort]: + tys = list(tys) + n = self.add_op(DummyOp(sops.UnpackTuple(parent=-1, tys=tys)), [port]) + + return [n.out(i) for i in range(len(tys))] + def _wire_up(self, node: Node, ports: Iterable[ToPort]): for i, p in enumerate(ports): src = p.to_port() diff --git a/hugr-py/tests/serialization/test_basic.py b/hugr-py/tests/serialization/test_basic.py index b9865fd3e..0da0f6d9f 100644 --- a/hugr-py/tests/serialization/test_basic.py +++ b/hugr-py/tests/serialization/test_basic.py @@ -8,7 +8,6 @@ NOT_OP = DummyOp( - # TODO get from YAML sops.CustomOp( parent=-1, extension="logic", @@ -17,6 +16,17 @@ ) ) +AND_OP = DummyOp( + # TODO get from YAML + sops.CustomOp( + parent=-1, + extension="logic", + op_name="And", + signature=stys.FunctionType(input=[BOOL_T] * 2, output=[BOOL_T]), + args=[stys.TypeArg(stys.BoundedNatArg(n=2))], + ) +) + def test_empty(): h = SerialHugr(nodes=[], edges=[]) @@ -67,3 +77,14 @@ def test_add_op(): h.set_outputs([nt]) _validate(h.hugr) + + +def test_tuple(): + row = [BOOL_T, QB_T] + h = Dfg.endo(row) + a, b = h.inputs() + t = h.make_tuple([a, b], row) + a, b = h.split_tuple(t, row) + h.set_outputs([a, b]) + + _validate(h.hugr) From 6d1f15b9d2c9aadcd5d01ba2e0cd0129e0ad718d Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 21 May 2024 13:59:17 +0100 Subject: [PATCH 16/57] slice based port unpacking --- hugr-py/src/hugr/hugr.py | 27 ++++++++++++++++++-- hugr-py/tests/serialization/test_basic.py | 31 ++++++++++++++++++----- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index b0df0bcff..17b808035 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, replace from collections.abc import Collection, Mapping -from typing import Iterable, Sequence, Protocol, Generic, TypeVar +from typing import Iterable, Sequence, Protocol, Generic, TypeVar, overload from hugr.serialization.serial_hugr import SerialHugr from hugr.serialization.ops import BaseOp, OpType as SerialOp @@ -36,6 +36,29 @@ def to_port(self) -> "OutPort": class Node(ToPort): idx: int + @overload + def __getitem__(self, index: int) -> OutPort: ... + @overload + def __getitem__(self, index: slice) -> Iterable[OutPort]: ... + @overload + def __getitem__(self, index: tuple[int, ...]) -> list[OutPort]: ... + + def __getitem__( + self, index: int | slice | tuple[int, ...] + ) -> OutPort | Iterable[OutPort]: + match index: + case int(index): + return self.out(index) + case slice(): + start = index.start or 0 + stop = index.stop + if stop is None: + raise ValueError("Stop must be specified") + step = index.step or 1 + return (self[i] for i in range(start, stop, step)) + case tuple(xs): + return [self[i] for i in xs] + def to_port(self) -> "OutPort": return OutPort(self, 0) @@ -106,7 +129,7 @@ def _unused_sub_offset(port: P, links: BiMap[OutPort, InPort]) -> P: return port -class Hugr(Mapping): +class Hugr(Mapping[Node, NodeData]): root: Node _nodes: list[NodeData | None] _links: BiMap[OutPort, InPort] diff --git a/hugr-py/tests/serialization/test_basic.py b/hugr-py/tests/serialization/test_basic.py index 0da0f6d9f..272e4f8d7 100644 --- a/hugr-py/tests/serialization/test_basic.py +++ b/hugr-py/tests/serialization/test_basic.py @@ -5,9 +5,18 @@ BOOL_T = stys.Type(stys.SumType(stys.UnitSum(size=2))) QB_T = stys.Type(stys.Qubit()) - +ARG_5 = stys.TypeArg(stys.BoundedNatArg(n=5)) +INT_T = stys.Type( + stys.Opaque( + extension="arithmetic.int.types", + id="int", + args=[ARG_5], + bound=stys.TypeBound.Eq, + ) +) NOT_OP = DummyOp( + # TODO get from YAML sops.CustomOp( parent=-1, extension="logic", @@ -16,14 +25,13 @@ ) ) -AND_OP = DummyOp( - # TODO get from YAML +DIV_OP = DummyOp( sops.CustomOp( parent=-1, - extension="logic", - op_name="And", - signature=stys.FunctionType(input=[BOOL_T] * 2, output=[BOOL_T]), - args=[stys.TypeArg(stys.BoundedNatArg(n=2))], + extension="arithmetic.int", + op_name="idivmod_u", + signature=stys.FunctionType(input=[INT_T] * 2, output=[INT_T] * 2), + args=[ARG_5, ARG_5], ) ) @@ -88,3 +96,12 @@ def test_tuple(): h.set_outputs([a, b]) _validate(h.hugr) + + +def test_multi_out(): + h = Dfg([INT_T] * 2, [INT_T] * 2) + a, b = h.inputs() + a, b = h.add_op(DIV_OP, [a, b])[:2] + h.set_outputs([a, b]) + + _validate(h.hugr, True) From c29c03581610d8c8674cb311e8b88d2c166a7655 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 21 May 2024 15:29:43 +0100 Subject: [PATCH 17/57] use tmp_path pytest fixture --- hugr-py/tests/serialization/test_basic.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/hugr-py/tests/serialization/test_basic.py b/hugr-py/tests/serialization/test_basic.py index 272e4f8d7..291676342 100644 --- a/hugr-py/tests/serialization/test_basic.py +++ b/hugr-py/tests/serialization/test_basic.py @@ -1,3 +1,4 @@ +import pytest from hugr.serialization import SerialHugr from hugr.hugr import Dfg, Hugr, DummyOp import hugr.serialization.tys as stys @@ -47,11 +48,20 @@ def test_empty(): } -def _validate(h: Hugr, mermaid: bool = False): +VALIDATE_DIR = None + + +@pytest.fixture(scope="session", autouse=True) +def validate_dir(tmp_path_factory: pytest.TempPathFactory) -> None: + global VALIDATE_DIR + VALIDATE_DIR = tmp_path_factory.mktemp("hugrs") + + +def _validate(h: Hugr, mermaid: bool = False, filename: str = "dump.hugr"): import subprocess - import tempfile - with tempfile.NamedTemporaryFile("w") as f: + assert VALIDATE_DIR is not None + with open(VALIDATE_DIR / filename, "w") as f: f.write(h.to_serial().to_json()) f.flush() # TODO point to built hugr binary @@ -104,4 +114,4 @@ def test_multi_out(): a, b = h.add_op(DIV_OP, [a, b])[:2] h.set_outputs([a, b]) - _validate(h.hugr, True) + _validate(h.hugr) From a163201a492815004b0ec530d805f138e8d222a0 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 21 May 2024 15:40:07 +0100 Subject: [PATCH 18/57] expand tuple test --- hugr-py/src/hugr/hugr.py | 5 +++-- hugr-py/tests/serialization/test_basic.py | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 17b808035..e5596db17 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -129,6 +129,7 @@ def _unused_sub_offset(port: P, links: BiMap[OutPort, InPort]) -> P: return port +@dataclass() class Hugr(Mapping[Node, NodeData]): root: Node _nodes: list[NodeData | None] @@ -302,11 +303,11 @@ def set_outputs(self, ports: Iterable[ToPort]) -> None: def make_tuple(self, ports: Iterable[ToPort], tys: Sequence[Type]) -> Node: ports = list(ports) assert len(tys) == len(ports), "Number of types must match number of ports" - return self.add_op(DummyOp(sops.MakeTuple(parent=-1, tys=list(tys))), ports) + return self.add_op(DummyOp(sops.MakeTuple(parent=0, tys=list(tys))), ports) def split_tuple(self, port: ToPort, tys: Sequence[Type]) -> list[OutPort]: tys = list(tys) - n = self.add_op(DummyOp(sops.UnpackTuple(parent=-1, tys=tys)), [port]) + n = self.add_op(DummyOp(sops.UnpackTuple(parent=0, tys=tys)), [port]) return [n.out(i) for i in range(len(tys))] diff --git a/hugr-py/tests/serialization/test_basic.py b/hugr-py/tests/serialization/test_basic.py index 291676342..9f10d1bc4 100644 --- a/hugr-py/tests/serialization/test_basic.py +++ b/hugr-py/tests/serialization/test_basic.py @@ -107,6 +107,14 @@ def test_tuple(): _validate(h.hugr) + h1 = Dfg.endo(row) + a, b = h1.inputs() + mt = h1.add_op(DummyOp(sops.MakeTuple(parent=-1, tys=row)), [a, b]) + a, b = h1.add_op(DummyOp(sops.UnpackTuple(parent=-1, tys=row)), [mt])[0, 1] + h1.set_outputs([a, b]) + + assert h.hugr.to_serial() == h1.hugr.to_serial() + def test_multi_out(): h = Dfg([INT_T] * 2, [INT_T] * 2) From 5095c156751a00c06826c27bd0f82119e56f7472 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 21 May 2024 15:57:29 +0100 Subject: [PATCH 19/57] remove num fields --- hugr-py/src/hugr/hugr.py | 40 +++++++--------------------------------- 1 file changed, 7 insertions(+), 33 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index e5596db17..b016b067c 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -88,32 +88,15 @@ def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: class NodeData: op: Op parent: Node | None - _n_in_ports: int = 0 - _n_out_ports: int = 0 # TODO children field? def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: o = self.op.to_serial(node, hugr) o.root.parent = self.parent.idx if self.parent else node.idx - # if all_not_none(self._in_ports) and all_not_none(self._out_ports): - # o.root.insert_port_types(self._in_ports, self._out_ports) return o -L = TypeVar("L") - - -def _insert_or_extend(lst: list[L | None], idx: int, value: L | None) -> None: - if idx >= len(lst): - lst.extend([None] * (idx - len(lst) + 1)) - lst[idx] = value - - -def _all_not_none(lst: list[L | None]) -> bool: - return all(i is not None for i in lst) - - P = TypeVar("P", InPort, OutPort) @@ -161,12 +144,9 @@ def add_node( self, op: Op, parent: Node | None = None, - n_inputs: int | None = None, - n_outputs: int | None = None, ) -> Node: parent = parent or self.root - # TODO add in_ports and out_ports - node_data = NodeData(op, parent, n_inputs or 0, n_outputs or 0) + node_data = NodeData(op, parent) if self._free_nodes: node = self._free_nodes.pop() @@ -190,29 +170,23 @@ def add_link(self, src: OutPort, dst: InPort, ty: Type | None = None) -> None: if self._links.get_left(dst) is not None: dst = replace(dst, sub_offset=dst.sub_offset + 1) self._links.insert_left(src, dst) - self[src.node]._n_out_ports = max(self[src.node]._n_out_ports, src.offset + 1) - self[dst.node]._n_in_ports = max(self[dst.node]._n_in_ports, dst.offset + 1) - # if ty is None: - # ty = self.port_type(src) - # if ty is None: - # ty = self.port_type(dst) - # _insert_or_extend(self[dst.node]._in_ports, dst.offset, ty) - # _insert_or_extend(self[src.node]._out_ports, src.offset, ty) def delete_link(self, src: OutPort, dst: InPort) -> None: self._links.delete_left(src) def num_in_ports(self, node: Node) -> int: - return self[node]._n_in_ports + return len(self.in_ports(node)) def num_out_ports(self, node: Node) -> int: - return self[node]._n_out_ports + return len(self.out_ports(node)) def in_ports(self, node: Node) -> Collection[InPort]: - return [node.inp(o) for o in range(self.num_in_ports(node))] + # can be optimised by caching number of ports + # or by guaranteeing that all ports are contiguous + return [p for p in self._links.bck if p.node == node] def out_ports(self, node: Node) -> Collection[OutPort]: - return [node.out(o) for o in range(self.num_out_ports(node))] + return [p for p in self._links.fwd if p.node == node] def insert_hugr(self, hugr: "Hugr", parent: Node | None = None) -> dict[Node, Node]: mapping: dict[Node, Node] = {} From 560cad7e4d48353be63fbee982e077f57761b11e Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 21 May 2024 17:26:32 +0100 Subject: [PATCH 20/57] get insert_nested working --- hugr-py/src/hugr/hugr.py | 24 ++++++++++++------- hugr-py/tests/serialization/test_basic.py | 29 ++++++++++++++++++++++- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index b016b067c..27ff4617d 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -120,10 +120,10 @@ class Hugr(Mapping[Node, NodeData]): _free_nodes: list[Node] def __init__(self, root_op: Op) -> None: - self.root = Node(0) - self._nodes = [NodeData(root_op, None)] - self._links = BiMap() self._free_nodes = [] + self._links = BiMap() + self._nodes = [] + self.root = self.add_node(root_op) def __getitem__(self, key: Node) -> NodeData: try: @@ -145,7 +145,6 @@ def add_node( op: Op, parent: Node | None = None, ) -> Node: - parent = parent or self.root node_data = NodeData(op, parent) if self._free_nodes: @@ -190,9 +189,16 @@ def out_ports(self, node: Node) -> Collection[OutPort]: def insert_hugr(self, hugr: "Hugr", parent: Node | None = None) -> dict[Node, Node]: mapping: dict[Node, Node] = {} - for idx, node_data in enumerate(self._nodes): + + for idx, node_data in enumerate(hugr._nodes): if node_data is not None: - mapping[Node(idx)] = self.add_node(node_data.op, parent) + mapping[Node(idx)] = self.add_node(node_data.op, node_data.parent) + + for new_node in mapping.values(): + # update mapped parent + node_data = self[new_node] + node_data.parent = mapping[node_data.parent] if node_data.parent else parent + for src, dst in hugr._links.items(): self.add_link( mapping[src.node].out(src.offset), mapping[dst.node].inp(dst.offset) @@ -232,10 +238,10 @@ def __init__( root_op._serial_op.signature.output = output_types self.hugr = Hugr(root_op) self.input_node = self.hugr.add_node( - DummyOp(sops.Input(parent=0, types=input_types)) + DummyOp(sops.Input(parent=0, types=input_types)), self.hugr.root ) self.output_node = self.hugr.add_node( - DummyOp(sops.Output(parent=0, types=output_types)) + DummyOp(sops.Output(parent=0, types=output_types)), self.hugr.root ) @classmethod @@ -255,7 +261,7 @@ def inputs(self) -> list[OutPort]: ] def add_op(self, op: Op, ports: Iterable[ToPort]) -> Node: - new_n = self.hugr.add_node(op) + new_n = self.hugr.add_node(op, self.hugr.root) self._wire_up(new_n, ports) return new_n diff --git a/hugr-py/tests/serialization/test_basic.py b/hugr-py/tests/serialization/test_basic.py index 9f10d1bc4..801a83b0d 100644 --- a/hugr-py/tests/serialization/test_basic.py +++ b/hugr-py/tests/serialization/test_basic.py @@ -1,6 +1,6 @@ import pytest from hugr.serialization import SerialHugr -from hugr.hugr import Dfg, Hugr, DummyOp +from hugr.hugr import Dfg, Hugr, DummyOp, Node import hugr.serialization.tys as stys import hugr.serialization.ops as sops @@ -123,3 +123,30 @@ def test_multi_out(): h.set_outputs([a, b]) _validate(h.hugr) + + +def test_insert(): + h1 = Dfg.endo([BOOL_T]) + (a1,) = h1.inputs() + nt = h1.add_op(NOT_OP, [a1]) + h1.set_outputs([nt]) + + assert len(h1.hugr) == 4 + + new_h = Hugr(DummyOp(sops.DFG(parent=-1))) + mapping = h1.hugr.insert_hugr(new_h, h1.hugr.root) + assert mapping == {new_h.root: Node(4)} + + +def test_nested(): + h1 = Dfg.endo([BOOL_T]) + (a1,) = h1.inputs() + nt = h1.add_op(NOT_OP, [a1]) + h1.set_outputs([nt]) + + h = Dfg.endo([BOOL_T]) + (a,) = h.inputs() + nested = h.insert_nested(h1, [a]) + h.set_outputs([nested]) + + _validate(h.hugr) From 8edda5681354460139f06ad0d976afa58d1440f4 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 21 May 2024 17:28:08 +0100 Subject: [PATCH 21/57] move into standalone test file --- hugr-py/tests/serialization/test_basic.py | 140 ---------------------- hugr-py/tests/test_hugr_build.py | 140 ++++++++++++++++++++++ 2 files changed, 140 insertions(+), 140 deletions(-) create mode 100644 hugr-py/tests/test_hugr_build.py diff --git a/hugr-py/tests/serialization/test_basic.py b/hugr-py/tests/serialization/test_basic.py index 801a83b0d..5c3b41ace 100644 --- a/hugr-py/tests/serialization/test_basic.py +++ b/hugr-py/tests/serialization/test_basic.py @@ -1,40 +1,4 @@ -import pytest from hugr.serialization import SerialHugr -from hugr.hugr import Dfg, Hugr, DummyOp, Node -import hugr.serialization.tys as stys -import hugr.serialization.ops as sops - -BOOL_T = stys.Type(stys.SumType(stys.UnitSum(size=2))) -QB_T = stys.Type(stys.Qubit()) -ARG_5 = stys.TypeArg(stys.BoundedNatArg(n=5)) -INT_T = stys.Type( - stys.Opaque( - extension="arithmetic.int.types", - id="int", - args=[ARG_5], - bound=stys.TypeBound.Eq, - ) -) - -NOT_OP = DummyOp( - # TODO get from YAML - sops.CustomOp( - parent=-1, - extension="logic", - op_name="Not", - signature=stys.FunctionType(input=[BOOL_T], output=[BOOL_T]), - ) -) - -DIV_OP = DummyOp( - sops.CustomOp( - parent=-1, - extension="arithmetic.int", - op_name="idivmod_u", - signature=stys.FunctionType(input=[INT_T] * 2, output=[INT_T] * 2), - args=[ARG_5, ARG_5], - ) -) def test_empty(): @@ -46,107 +10,3 @@ def test_empty(): "metadata": None, "encoder": None, } - - -VALIDATE_DIR = None - - -@pytest.fixture(scope="session", autouse=True) -def validate_dir(tmp_path_factory: pytest.TempPathFactory) -> None: - global VALIDATE_DIR - VALIDATE_DIR = tmp_path_factory.mktemp("hugrs") - - -def _validate(h: Hugr, mermaid: bool = False, filename: str = "dump.hugr"): - import subprocess - - assert VALIDATE_DIR is not None - with open(VALIDATE_DIR / filename, "w") as f: - f.write(h.to_serial().to_json()) - f.flush() - # TODO point to built hugr binary - cmd = ["cargo", "run", "--"] - - if mermaid: - cmd.append("--mermaid") - subprocess.run(cmd + [f.name], check=True) - - -def test_simple_id(): - h = Dfg.endo([QB_T] * 2) - a, b = h.inputs() - h.set_outputs([a, b]) - - _validate(h.hugr) - - -def test_multiport(): - h = Dfg([BOOL_T], [BOOL_T] * 2) - (a,) = h.inputs() - h.set_outputs([a, a]) - - _validate(h.hugr) - - -def test_add_op(): - h = Dfg.endo([BOOL_T]) - (a,) = h.inputs() - nt = h.add_op(NOT_OP, [a]) - h.set_outputs([nt]) - - _validate(h.hugr) - - -def test_tuple(): - row = [BOOL_T, QB_T] - h = Dfg.endo(row) - a, b = h.inputs() - t = h.make_tuple([a, b], row) - a, b = h.split_tuple(t, row) - h.set_outputs([a, b]) - - _validate(h.hugr) - - h1 = Dfg.endo(row) - a, b = h1.inputs() - mt = h1.add_op(DummyOp(sops.MakeTuple(parent=-1, tys=row)), [a, b]) - a, b = h1.add_op(DummyOp(sops.UnpackTuple(parent=-1, tys=row)), [mt])[0, 1] - h1.set_outputs([a, b]) - - assert h.hugr.to_serial() == h1.hugr.to_serial() - - -def test_multi_out(): - h = Dfg([INT_T] * 2, [INT_T] * 2) - a, b = h.inputs() - a, b = h.add_op(DIV_OP, [a, b])[:2] - h.set_outputs([a, b]) - - _validate(h.hugr) - - -def test_insert(): - h1 = Dfg.endo([BOOL_T]) - (a1,) = h1.inputs() - nt = h1.add_op(NOT_OP, [a1]) - h1.set_outputs([nt]) - - assert len(h1.hugr) == 4 - - new_h = Hugr(DummyOp(sops.DFG(parent=-1))) - mapping = h1.hugr.insert_hugr(new_h, h1.hugr.root) - assert mapping == {new_h.root: Node(4)} - - -def test_nested(): - h1 = Dfg.endo([BOOL_T]) - (a1,) = h1.inputs() - nt = h1.add_op(NOT_OP, [a1]) - h1.set_outputs([nt]) - - h = Dfg.endo([BOOL_T]) - (a,) = h.inputs() - nested = h.insert_nested(h1, [a]) - h.set_outputs([nested]) - - _validate(h.hugr) diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py new file mode 100644 index 000000000..8080fac75 --- /dev/null +++ b/hugr-py/tests/test_hugr_build.py @@ -0,0 +1,140 @@ +import pytest +from hugr.hugr import Dfg, Hugr, DummyOp, Node +import hugr.serialization.tys as stys +import hugr.serialization.ops as sops + +BOOL_T = stys.Type(stys.SumType(stys.UnitSum(size=2))) +QB_T = stys.Type(stys.Qubit()) +ARG_5 = stys.TypeArg(stys.BoundedNatArg(n=5)) +INT_T = stys.Type( + stys.Opaque( + extension="arithmetic.int.types", + id="int", + args=[ARG_5], + bound=stys.TypeBound.Eq, + ) +) + +NOT_OP = DummyOp( + # TODO get from YAML + sops.CustomOp( + parent=-1, + extension="logic", + op_name="Not", + signature=stys.FunctionType(input=[BOOL_T], output=[BOOL_T]), + ) +) + +DIV_OP = DummyOp( + sops.CustomOp( + parent=-1, + extension="arithmetic.int", + op_name="idivmod_u", + signature=stys.FunctionType(input=[INT_T] * 2, output=[INT_T] * 2), + args=[ARG_5, ARG_5], + ) +) + + +VALIDATE_DIR = None + + +@pytest.fixture(scope="session", autouse=True) +def validate_dir(tmp_path_factory: pytest.TempPathFactory) -> None: + global VALIDATE_DIR + VALIDATE_DIR = tmp_path_factory.mktemp("hugrs") + + +def _validate(h: Hugr, mermaid: bool = False, filename: str = "dump.hugr"): + import subprocess + + assert VALIDATE_DIR is not None + with open(VALIDATE_DIR / filename, "w") as f: + f.write(h.to_serial().to_json()) + f.flush() + # TODO point to built hugr binary + cmd = ["cargo", "run", "--"] + + if mermaid: + cmd.append("--mermaid") + subprocess.run(cmd + [f.name], check=True) + + +def test_simple_id(): + h = Dfg.endo([QB_T] * 2) + a, b = h.inputs() + h.set_outputs([a, b]) + + _validate(h.hugr) + + +def test_multiport(): + h = Dfg([BOOL_T], [BOOL_T] * 2) + (a,) = h.inputs() + h.set_outputs([a, a]) + + _validate(h.hugr) + + +def test_add_op(): + h = Dfg.endo([BOOL_T]) + (a,) = h.inputs() + nt = h.add_op(NOT_OP, [a]) + h.set_outputs([nt]) + + _validate(h.hugr) + + +def test_tuple(): + row = [BOOL_T, QB_T] + h = Dfg.endo(row) + a, b = h.inputs() + t = h.make_tuple([a, b], row) + a, b = h.split_tuple(t, row) + h.set_outputs([a, b]) + + _validate(h.hugr) + + h1 = Dfg.endo(row) + a, b = h1.inputs() + mt = h1.add_op(DummyOp(sops.MakeTuple(parent=-1, tys=row)), [a, b]) + a, b = h1.add_op(DummyOp(sops.UnpackTuple(parent=-1, tys=row)), [mt])[0, 1] + h1.set_outputs([a, b]) + + assert h.hugr.to_serial() == h1.hugr.to_serial() + + +def test_multi_out(): + h = Dfg([INT_T] * 2, [INT_T] * 2) + a, b = h.inputs() + a, b = h.add_op(DIV_OP, [a, b])[:2] + h.set_outputs([a, b]) + + _validate(h.hugr) + + +def test_insert(): + h1 = Dfg.endo([BOOL_T]) + (a1,) = h1.inputs() + nt = h1.add_op(NOT_OP, [a1]) + h1.set_outputs([nt]) + + assert len(h1.hugr) == 4 + + new_h = Hugr(DummyOp(sops.DFG(parent=-1))) + mapping = h1.hugr.insert_hugr(new_h, h1.hugr.root) + assert mapping == {new_h.root: Node(4)} + + +def test_nested(): + h1 = Dfg.endo([BOOL_T]) + (a1,) = h1.inputs() + nt = h1.add_op(NOT_OP, [a1]) + h1.set_outputs([nt]) + + h = Dfg.endo([BOOL_T]) + (a,) = h.inputs() + nested = h.insert_nested(h1, [a]) + h.set_outputs([nested]) + + _validate(h.hugr) From 6fb7419b6ea8e6d565057e08a984ce6dcb611a96 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 22 May 2024 10:34:18 +0100 Subject: [PATCH 22/57] nested dfg building --- hugr-py/src/hugr/hugr.py | 29 ++++++++++++++++++++--------- hugr-py/tests/test_hugr_build.py | 19 ++++++++++++++++++- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 27ff4617d..ea393a373 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -225,6 +225,7 @@ def from_serial(cls, serial: SerialHugr) -> "Hugr": @dataclass() class Dfg: hugr: Hugr + root: Node input_node: Node output_node: Node @@ -237,11 +238,12 @@ def __init__( root_op._serial_op.signature.input = input_types root_op._serial_op.signature.output = output_types self.hugr = Hugr(root_op) + self.root = self.hugr.root self.input_node = self.hugr.add_node( - DummyOp(sops.Input(parent=0, types=input_types)), self.hugr.root + DummyOp(sops.Input(parent=0, types=input_types)), self.root ) self.output_node = self.hugr.add_node( - DummyOp(sops.Output(parent=0, types=output_types)), self.hugr.root + DummyOp(sops.Output(parent=0, types=output_types)), self.root ) @classmethod @@ -261,19 +263,28 @@ def inputs(self) -> list[OutPort]: ] def add_op(self, op: Op, ports: Iterable[ToPort]) -> Node: - new_n = self.hugr.add_node(op, self.hugr.root) + new_n = self.hugr.add_node(op, self.root) self._wire_up(new_n, ports) return new_n def insert_nested(self, dfg: "Dfg", ports: Iterable[ToPort]) -> Node: - mapping = self.hugr.insert_hugr(dfg.hugr, self.hugr.root) - self._wire_up(mapping[dfg.hugr.root], ports) - return mapping[dfg.hugr.root] + mapping = self.hugr.insert_hugr(dfg.hugr, self.root) + self._wire_up(mapping[dfg.root], ports) + return mapping[dfg.root] - def add_nested(self, ports: Iterable[ToPort]) -> "Dfg": - dfg = Dfg.__new__(Dfg) + def add_nested( + self, + input_types: Sequence[Type], + output_types: Sequence[Type], + ports: Iterable[ToPort], + ) -> "Dfg": + dfg = Dfg(input_types, output_types) + mapping = self.hugr.insert_hugr(dfg.hugr, self.root) + self._wire_up(mapping[dfg.root], ports) dfg.hugr = self.hugr - # I/O nodes + dfg.input_node = mapping[dfg.input_node] + dfg.output_node = mapping[dfg.output_node] + dfg.root = mapping[dfg.root] return dfg diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index 8080fac75..c29865649 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -126,7 +126,7 @@ def test_insert(): assert mapping == {new_h.root: Node(4)} -def test_nested(): +def test_insert_nested(): h1 = Dfg.endo([BOOL_T]) (a1,) = h1.inputs() nt = h1.add_op(NOT_OP, [a1]) @@ -138,3 +138,20 @@ def test_nested(): h.set_outputs([nested]) _validate(h.hugr) + + +def test_build_nested(): + def _nested_nop(dfg: Dfg): + (a1,) = dfg.inputs() + nt = dfg.add_op(NOT_OP, [a1]) + dfg.set_outputs([nt]) + + h = Dfg.endo([BOOL_T]) + (a,) = h.inputs() + nested = h.add_nested([BOOL_T], [BOOL_T], [a]) + + _nested_nop(nested) + + h.set_outputs([nested.root]) + + _validate(h.hugr) From 784e7ef415b828bd34f011539a02c77c8304aa2c Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 22 May 2024 12:43:46 +0100 Subject: [PATCH 23/57] validate using stdin --- hugr-py/tests/test_hugr_build.py | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index c29865649..cf4de3a20 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -1,4 +1,5 @@ -import pytest +import subprocess + from hugr.hugr import Dfg, Hugr, DummyOp, Node import hugr.serialization.tys as stys import hugr.serialization.ops as sops @@ -36,28 +37,13 @@ ) -VALIDATE_DIR = None - - -@pytest.fixture(scope="session", autouse=True) -def validate_dir(tmp_path_factory: pytest.TempPathFactory) -> None: - global VALIDATE_DIR - VALIDATE_DIR = tmp_path_factory.mktemp("hugrs") - - -def _validate(h: Hugr, mermaid: bool = False, filename: str = "dump.hugr"): - import subprocess - - assert VALIDATE_DIR is not None - with open(VALIDATE_DIR / filename, "w") as f: - f.write(h.to_serial().to_json()) - f.flush() - # TODO point to built hugr binary - cmd = ["cargo", "run", "--"] +def _validate(h: Hugr, mermaid: bool = False): + # TODO point to built hugr binary + cmd = ["cargo", "run", "--features", "cli", "--"] - if mermaid: - cmd.append("--mermaid") - subprocess.run(cmd + [f.name], check=True) + if mermaid: + cmd.append("--mermaid") + subprocess.run(cmd + ["-"], check=True, input=h.to_serial().to_json().encode()) def test_simple_id(): From 2c0d7a5f75d882da8b0fecc8a5f07ecfb5aded28 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 22 May 2024 13:37:51 +0100 Subject: [PATCH 24/57] use built binary --- hugr-py/tests/test_hugr_build.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index cf4de3a20..03092b072 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -39,7 +39,8 @@ def _validate(h: Hugr, mermaid: bool = False): # TODO point to built hugr binary - cmd = ["cargo", "run", "--features", "cli", "--"] + # cmd = ["cargo", "run", "--features", "cli", "--"] + cmd = ["./target/debug/hugr"] if mermaid: cmd.append("--mermaid") From 2a6f80f3f40e48ac270e25cfa5f0e362aadf69dd Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 22 May 2024 14:27:12 +0100 Subject: [PATCH 25/57] test stable indices --- hugr-py/src/hugr/hugr.py | 13 ++++++++---- hugr-py/tests/test_hugr_build.py | 35 +++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index ea393a373..6a26f76d7 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -138,7 +138,7 @@ def __iter__(self): return iter(self._nodes) def __len__(self) -> int: - return len(self._nodes) - len(self._free_nodes) + return self.num_nodes() def add_node( self, @@ -155,15 +155,17 @@ def add_node( self._nodes.append(node_data) return node - def delete_node(self, node: Node) -> None: + def delete_node(self, node: Node) -> NodeData | None: for offset in range(self.num_in_ports(node)): self._links.delete_right(node.inp(offset)) for offset in range(self.num_out_ports(node)): self._links.delete_left(node.out(offset)) - self._nodes[node.idx] = None + + weight, self._nodes[node.idx] = self._nodes[node.idx], None self._free_nodes.append(node) + return weight - def add_link(self, src: OutPort, dst: InPort, ty: Type | None = None) -> None: + def add_link(self, src: OutPort, dst: InPort) -> None: src = _unused_sub_offset(src, self._links) dst = _unused_sub_offset(dst, self._links) if self._links.get_left(dst) is not None: @@ -173,6 +175,9 @@ def add_link(self, src: OutPort, dst: InPort, ty: Type | None = None) -> None: def delete_link(self, src: OutPort, dst: InPort) -> None: self._links.delete_left(src) + def num_nodes(self) -> int: + return len(self._nodes) - len(self._free_nodes) + def num_in_ports(self, node: Node) -> int: return len(self.in_ports(node)) diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index 03092b072..80379ef50 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -3,6 +3,7 @@ from hugr.hugr import Dfg, Hugr, DummyOp, Node import hugr.serialization.tys as stys import hugr.serialization.ops as sops +import pytest BOOL_T = stys.Type(stys.SumType(stys.UnitSum(size=2))) QB_T = stys.Type(stys.Qubit()) @@ -38,7 +39,6 @@ def _validate(h: Hugr, mermaid: bool = False): - # TODO point to built hugr binary # cmd = ["cargo", "run", "--features", "cli", "--"] cmd = ["./target/debug/hugr"] @@ -47,6 +47,39 @@ def _validate(h: Hugr, mermaid: bool = False): subprocess.run(cmd + ["-"], check=True, input=h.to_serial().to_json().encode()) +def test_stable_indices(): + h = Hugr(DummyOp(sops.DFG(parent=-1))) + + nodes = [h.add_node(NOT_OP) for _ in range(3)] + assert len(h) == 4 + + h.add_link(nodes[0].out(0), nodes[1].inp(0)) + + assert h.num_out_ports(nodes[0]) == 1 + assert h.num_in_ports(nodes[1]) == 1 + + assert h.delete_node(nodes[1]) is not None + assert h._nodes[nodes[1].idx] is None + + assert len(h) == 3 + assert len(h._nodes) == 4 + assert h._free_nodes == [nodes[1]] + + assert h.num_out_ports(nodes[0]) == 0 + assert h.num_in_ports(nodes[1]) == 0 + + with pytest.raises(KeyError): + _ = h[nodes[1]] + with pytest.raises(KeyError): + _ = h[Node(46)] + + new_n = h.add_node(NOT_OP) + assert new_n == nodes[1] + + assert len(h) == 4 + assert h._free_nodes == [] + + def test_simple_id(): h = Dfg.endo([QB_T] * 2) a, b = h.inputs() From a0992984cde955ed052ab4794a747a4092f75f27 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 22 May 2024 16:14:25 +0100 Subject: [PATCH 26/57] use varargs for wires to save some chars --- hugr-py/src/hugr/hugr.py | 22 ++++++++--------- hugr-py/tests/test_hugr_build.py | 42 ++++++++++++++++---------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 6a26f76d7..b9c71b17e 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -267,14 +267,14 @@ def inputs(self) -> list[OutPort]: for i in range(len(self._input_op()._serial_op.types)) ] - def add_op(self, op: Op, ports: Iterable[ToPort]) -> Node: + def add_op(self, op: Op, /, *args: ToPort) -> Node: new_n = self.hugr.add_node(op, self.root) - self._wire_up(new_n, ports) + self._wire_up(new_n, args) return new_n - def insert_nested(self, dfg: "Dfg", ports: Iterable[ToPort]) -> Node: + def insert_nested(self, dfg: "Dfg", *args: ToPort) -> Node: mapping = self.hugr.insert_hugr(dfg.hugr, self.root) - self._wire_up(mapping[dfg.root], ports) + self._wire_up(mapping[dfg.root], args) return mapping[dfg.root] def add_nested( @@ -293,17 +293,17 @@ def add_nested( return dfg - def set_outputs(self, ports: Iterable[ToPort]) -> None: - self._wire_up(self.output_node, ports) + def set_outputs(self, *args: ToPort) -> None: + self._wire_up(self.output_node, args) - def make_tuple(self, ports: Iterable[ToPort], tys: Sequence[Type]) -> Node: - ports = list(ports) + def make_tuple(self, tys: Sequence[Type], *args: ToPort) -> Node: + ports = list(args) assert len(tys) == len(ports), "Number of types must match number of ports" - return self.add_op(DummyOp(sops.MakeTuple(parent=0, tys=list(tys))), ports) + return self.add_op(DummyOp(sops.MakeTuple(parent=0, tys=list(tys))), *args) - def split_tuple(self, port: ToPort, tys: Sequence[Type]) -> list[OutPort]: + def split_tuple(self, tys: Sequence[Type], port: ToPort) -> list[OutPort]: tys = list(tys) - n = self.add_op(DummyOp(sops.UnpackTuple(parent=0, tys=tys)), [port]) + n = self.add_op(DummyOp(sops.UnpackTuple(parent=0, tys=tys)), port) return [n.out(i) for i in range(len(tys))] diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index 80379ef50..4220a15e7 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -83,7 +83,7 @@ def test_stable_indices(): def test_simple_id(): h = Dfg.endo([QB_T] * 2) a, b = h.inputs() - h.set_outputs([a, b]) + h.set_outputs(a, b) _validate(h.hugr) @@ -91,7 +91,7 @@ def test_simple_id(): def test_multiport(): h = Dfg([BOOL_T], [BOOL_T] * 2) (a,) = h.inputs() - h.set_outputs([a, a]) + h.set_outputs(a, a) _validate(h.hugr) @@ -99,8 +99,8 @@ def test_multiport(): def test_add_op(): h = Dfg.endo([BOOL_T]) (a,) = h.inputs() - nt = h.add_op(NOT_OP, [a]) - h.set_outputs([nt]) + nt = h.add_op(NOT_OP, a) + h.set_outputs(nt) _validate(h.hugr) @@ -109,17 +109,17 @@ def test_tuple(): row = [BOOL_T, QB_T] h = Dfg.endo(row) a, b = h.inputs() - t = h.make_tuple([a, b], row) - a, b = h.split_tuple(t, row) - h.set_outputs([a, b]) + t = h.make_tuple(row, a, b) + a, b = h.split_tuple(row, t) + h.set_outputs(a, b) _validate(h.hugr) h1 = Dfg.endo(row) a, b = h1.inputs() - mt = h1.add_op(DummyOp(sops.MakeTuple(parent=-1, tys=row)), [a, b]) - a, b = h1.add_op(DummyOp(sops.UnpackTuple(parent=-1, tys=row)), [mt])[0, 1] - h1.set_outputs([a, b]) + mt = h1.add_op(DummyOp(sops.MakeTuple(parent=-1, tys=row)), a, b) + a, b = h1.add_op(DummyOp(sops.UnpackTuple(parent=-1, tys=row)), mt)[0, 1] + h1.set_outputs(a, b) assert h.hugr.to_serial() == h1.hugr.to_serial() @@ -127,8 +127,8 @@ def test_tuple(): def test_multi_out(): h = Dfg([INT_T] * 2, [INT_T] * 2) a, b = h.inputs() - a, b = h.add_op(DIV_OP, [a, b])[:2] - h.set_outputs([a, b]) + a, b = h.add_op(DIV_OP, a, b)[:2] + h.set_outputs(a, b) _validate(h.hugr) @@ -136,8 +136,8 @@ def test_multi_out(): def test_insert(): h1 = Dfg.endo([BOOL_T]) (a1,) = h1.inputs() - nt = h1.add_op(NOT_OP, [a1]) - h1.set_outputs([nt]) + nt = h1.add_op(NOT_OP, a1) + h1.set_outputs(nt) assert len(h1.hugr) == 4 @@ -149,13 +149,13 @@ def test_insert(): def test_insert_nested(): h1 = Dfg.endo([BOOL_T]) (a1,) = h1.inputs() - nt = h1.add_op(NOT_OP, [a1]) - h1.set_outputs([nt]) + nt = h1.add_op(NOT_OP, a1) + h1.set_outputs(nt) h = Dfg.endo([BOOL_T]) (a,) = h.inputs() - nested = h.insert_nested(h1, [a]) - h.set_outputs([nested]) + nested = h.insert_nested(h1, a) + h.set_outputs(nested) _validate(h.hugr) @@ -163,8 +163,8 @@ def test_insert_nested(): def test_build_nested(): def _nested_nop(dfg: Dfg): (a1,) = dfg.inputs() - nt = dfg.add_op(NOT_OP, [a1]) - dfg.set_outputs([nt]) + nt = dfg.add_op(NOT_OP, a1) + dfg.set_outputs(nt) h = Dfg.endo([BOOL_T]) (a,) = h.inputs() @@ -172,6 +172,6 @@ def _nested_nop(dfg: Dfg): _nested_nop(nested) - h.set_outputs([nested.root]) + h.set_outputs(nested.root) _validate(h.hugr) From 944cd0d45537044cde12fe79cfb9db0e59a60e64 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 22 May 2024 17:50:42 +0100 Subject: [PATCH 27/57] dodgy support for commands --- hugr-py/src/hugr/hugr.py | 33 +++++++++++++++---- hugr-py/tests/test_hugr_build.py | 56 ++++++++++++++++++++++++-------- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index b9c71b17e..bde6f3de4 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, replace +from dataclasses import dataclass, field, replace from collections.abc import Collection, Mapping from typing import Iterable, Sequence, Protocol, Generic, TypeVar, overload @@ -35,6 +35,7 @@ def to_port(self) -> "OutPort": @dataclass(frozen=True, eq=True, order=True) class Node(ToPort): idx: int + _num_out_ports: int | None = field(default=None, compare=False) @overload def __getitem__(self, index: int) -> OutPort: ... @@ -48,12 +49,17 @@ def __getitem__( ) -> OutPort | Iterable[OutPort]: match index: case int(index): + if self._num_out_ports is not None: + if index >= self._num_out_ports: + raise IndexError("Index out of range") return self.out(index) case slice(): start = index.start or 0 - stop = index.stop + stop = index.stop or self._num_out_ports if stop is None: - raise ValueError("Stop must be specified") + raise ValueError( + "Stop must be specified when number of outputs unknown" + ) step = index.step or 1 return (self[i] for i in range(start, stop, step)) case tuple(xs): @@ -84,6 +90,13 @@ def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: return SerialOp(root=self._serial_op) # type: ignore +class Command(Protocol): + def op(self) -> Op: ... + def incoming(self) -> Iterable[ToPort]: ... + def num_out(self) -> int | None: + return None + + @dataclass() class NodeData: op: Op @@ -144,6 +157,7 @@ def add_node( self, op: Op, parent: Node | None = None, + num_outs: int | None = None, ) -> Node: node_data = NodeData(op, parent) @@ -153,7 +167,7 @@ def add_node( else: node = Node(len(self._nodes)) self._nodes.append(node_data) - return node + return replace(node, _num_out_ports=num_outs) def delete_node(self, node: Node) -> NodeData | None: for offset in range(self.num_in_ports(node)): @@ -245,7 +259,9 @@ def __init__( self.hugr = Hugr(root_op) self.root = self.hugr.root self.input_node = self.hugr.add_node( - DummyOp(sops.Input(parent=0, types=input_types)), self.root + DummyOp(sops.Input(parent=0, types=input_types)), + self.root, + len(input_types), ) self.output_node = self.hugr.add_node( DummyOp(sops.Output(parent=0, types=output_types)), self.root @@ -267,11 +283,14 @@ def inputs(self) -> list[OutPort]: for i in range(len(self._input_op()._serial_op.types)) ] - def add_op(self, op: Op, /, *args: ToPort) -> Node: - new_n = self.hugr.add_node(op, self.root) + def add_op(self, op: Op, /, *args: ToPort, num_outs: int | None = None) -> Node: + new_n = self.hugr.add_node(op, self.root, num_outs=num_outs) self._wire_up(new_n, args) return new_n + def add(self, com: Command) -> Node: + return self.add_op(com.op(), *com.incoming(), num_outs=com.num_out()) + def insert_nested(self, dfg: "Dfg", *args: ToPort) -> Node: mapping = self.hugr.insert_hugr(dfg.hugr, self.root) self._wire_up(mapping[dfg.root], args) diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index 4220a15e7..a4bef3faa 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -1,6 +1,7 @@ +from dataclasses import dataclass import subprocess -from hugr.hugr import Dfg, Hugr, DummyOp, Node +from hugr.hugr import Dfg, Hugr, DummyOp, Node, Command, ToPort, Op import hugr.serialization.tys as stys import hugr.serialization.ops as sops import pytest @@ -27,15 +28,42 @@ ) ) -DIV_OP = DummyOp( - sops.CustomOp( - parent=-1, - extension="arithmetic.int", - op_name="idivmod_u", - signature=stys.FunctionType(input=[INT_T] * 2, output=[INT_T] * 2), - args=[ARG_5, ARG_5], - ) -) + +@dataclass +class Not(Command): + a: ToPort + + def incoming(self) -> list[ToPort]: + return [self.a] + + def num_out(self) -> int | None: + return 1 + + def op(self) -> Op: + return NOT_OP + + +@dataclass +class DivMod(Command): + a: ToPort + b: ToPort + + def incoming(self) -> list[ToPort]: + return [self.a, self.b] + + def num_out(self) -> int | None: + return 2 + + def op(self) -> Op: + return DummyOp( + sops.CustomOp( + parent=-1, + extension="arithmetic.int", + op_name="idivmod_u", + signature=stys.FunctionType(input=[INT_T] * 2, output=[INT_T] * 2), + args=[ARG_5, ARG_5], + ) + ) def _validate(h: Hugr, mermaid: bool = False): @@ -127,7 +155,7 @@ def test_tuple(): def test_multi_out(): h = Dfg([INT_T] * 2, [INT_T] * 2) a, b = h.inputs() - a, b = h.add_op(DIV_OP, a, b)[:2] + a, b = h.add(DivMod(a, b)) h.set_outputs(a, b) _validate(h.hugr) @@ -136,7 +164,7 @@ def test_multi_out(): def test_insert(): h1 = Dfg.endo([BOOL_T]) (a1,) = h1.inputs() - nt = h1.add_op(NOT_OP, a1) + nt = h1.add(Not(a1)) h1.set_outputs(nt) assert len(h1.hugr) == 4 @@ -149,7 +177,7 @@ def test_insert(): def test_insert_nested(): h1 = Dfg.endo([BOOL_T]) (a1,) = h1.inputs() - nt = h1.add_op(NOT_OP, a1) + nt = h1.add(Not(a1)) h1.set_outputs(nt) h = Dfg.endo([BOOL_T]) @@ -163,7 +191,7 @@ def test_insert_nested(): def test_build_nested(): def _nested_nop(dfg: Dfg): (a1,) = dfg.inputs() - nt = dfg.add_op(NOT_OP, a1) + nt = dfg.add(Not(a1)) dfg.set_outputs(nt) h = Dfg.endo([BOOL_T]) From 40fc9989319b33ac7c9dc851d9eaa620db234867 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 24 May 2024 12:19:44 +0100 Subject: [PATCH 28/57] ci: cargo build before test --- .github/workflows/ci-py.yml | 7 ++++++- hugr-py/tests/test_hugr_build.py | 11 +++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index dfff0d309..6adf4e6a3 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -69,7 +69,12 @@ jobs: run: poetry run ruff check - name: Run tests - run: poetry run pytest + env: + HUGR_BIN: ${{ github.workspace }}/target/debug/hugr + # have to build hugr binary for validation tests + run: | + cargo build --features cli --bin hugr + poetry run pytest # Ensure that the serialization schema is up to date serialization-schema: diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index a4bef3faa..dcc7a57b9 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -1,6 +1,7 @@ from dataclasses import dataclass import subprocess - +import os +import pathlib from hugr.hugr import Dfg, Hugr, DummyOp, Node, Command, ToPort, Op import hugr.serialization.tys as stys import hugr.serialization.ops as sops @@ -67,12 +68,14 @@ def op(self) -> Op: def _validate(h: Hugr, mermaid: bool = False): - # cmd = ["cargo", "run", "--features", "cli", "--"] - cmd = ["./target/debug/hugr"] + workspace_dir = pathlib.Path(__file__).parent.parent.parent + # use the HUGR_BIN environment variable if set, otherwise use the debug build + bin_loc = os.environ.get("HUGR_BIN", str(workspace_dir / "target/debug/hugr")) + cmd = [bin_loc, "-"] if mermaid: cmd.append("--mermaid") - subprocess.run(cmd + ["-"], check=True, input=h.to_serial().to_json().encode()) + subprocess.run(cmd, check=True, input=h.to_serial().to_json().encode()) def test_stable_indices(): From 1e4223f242133c8dcab29345ee77393bee1d1998 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 24 May 2024 12:43:03 +0100 Subject: [PATCH 29/57] ci: use built binary in coverage check --- .github/workflows/ci-py.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index 6adf4e6a3..d6218791b 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -68,13 +68,19 @@ jobs: - name: Lint with ruff run: poetry run ruff check + - name: Build HUGR binary + # have to build hugr binary for validation tests + run: cargo build --features cli --bin hugr - name: Run tests env: HUGR_BIN: ${{ github.workspace }}/target/debug/hugr - # have to build hugr binary for validation tests - run: | - cargo build --features cli --bin hugr - poetry run pytest + run: poetry run pytest + + - name: Upload the binary to the artifacts + uses: actions/upload-artifact@v4 + with: + name: hugr-binary + path: target/debug/hugr # Ensure that the serialization schema is up to date serialization-schema: @@ -151,8 +157,14 @@ jobs: - name: Install the project libraries run: poetry install + - name: Download the hugr binary + uses: actions/download-artifact@v4 + with: + name: hugr-binary - name: Run python tests with coverage instrumentation + env: + HUGR_BIN: ${{ github.workspace }}/target/debug/hugr run: poetry run pytest --cov=./ --cov-report=xml - name: Upload python coverage to codecov.io From 08bd1a5313479e8f6eed03eb265b5e1d298e90c9 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 24 May 2024 12:52:59 +0100 Subject: [PATCH 30/57] ci: separate build step --- .github/workflows/ci-py.yml | 55 ++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index d6218791b..afdeb711c 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -13,6 +13,7 @@ on: env: SCCACHE_GHA_ENABLED: "true" + HUGR_BIN: ${{ github.workspace }}/target/debug/hugr jobs: # Check if changes were made to the relevant files. @@ -68,19 +69,55 @@ jobs: - name: Lint with ruff run: poetry run ruff check + build_binary: + needs: changes + if: ${{ needs.changes.outputs.python == 'true' }} + + name: Build HUGR binary + runs-on: ubuntu-latest + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + + steps: + - uses: actions/checkout@v4 + - uses: mozilla-actions/sccache-action@v0.0.4 + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable - name: Build HUGR binary - # have to build hugr binary for validation tests run: cargo build --features cli --bin hugr - - name: Run tests - env: - HUGR_BIN: ${{ github.workspace }}/target/debug/hugr - run: poetry run pytest - - name: Upload the binary to the artifacts uses: actions/upload-artifact@v4 with: name: hugr-binary - path: target/debug/hugr + path: ${{env.HUGR_BIN}} + test: + needs: [changes, check, build_binary] + if: ${{ needs.changes.outputs.python == 'true' }} + name: check python ${{ matrix.python-version }} + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ['3.10', '3.12'] + steps: + - uses: actions/checkout@v4 + - name: Install poetry + run: pipx install poetry + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "poetry" + + - name: Install the project libraries + run: poetry install + - name: Download the hugr binary + uses: actions/download-artifact@v4 + with: + name: hugr-binary + - name: Run tests + run: poetry run pytest # Ensure that the serialization schema is up to date serialization-schema: @@ -139,7 +176,7 @@ jobs: echo "All required checks passed" coverage: - needs: [changes, check] + needs: [changes, check, build_binary] # Run only if there are changes in the relevant files and the check job passed or was skipped if: always() && !failure() && !cancelled() && needs.changes.outputs.python == 'true' && github.event_name != 'merge_group' runs-on: ubuntu-latest @@ -163,8 +200,6 @@ jobs: name: hugr-binary - name: Run python tests with coverage instrumentation - env: - HUGR_BIN: ${{ github.workspace }}/target/debug/hugr run: poetry run pytest --cov=./ --cov-report=xml - name: Upload python coverage to codecov.io From 34cf38e580868aac8bd3f2d1806102971a0f92b3 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 24 May 2024 12:56:42 +0100 Subject: [PATCH 31/57] ci: make test requires --- .github/workflows/ci-py.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index afdeb711c..16c672481 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -13,7 +13,7 @@ on: env: SCCACHE_GHA_ENABLED: "true" - HUGR_BIN: ${{ github.workspace }}/target/debug/hugr + HUGR_BIN: ${{ github.workspace }}/target/release/hugr jobs: # Check if changes were made to the relevant files. @@ -85,11 +85,11 @@ jobs: - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable - name: Build HUGR binary - run: cargo build --features cli --bin hugr + run: cargo build --release --features cli --bin hugr - name: Upload the binary to the artifacts uses: actions/upload-artifact@v4 with: - name: hugr-binary + name: hugr_binary path: ${{env.HUGR_BIN}} test: needs: [changes, check, build_binary] @@ -115,7 +115,9 @@ jobs: - name: Download the hugr binary uses: actions/download-artifact@v4 with: - name: hugr-binary + name: hugr_binary + path: ${{env.HUGR_BIN}} + - name: Run tests run: poetry run pytest @@ -154,7 +156,7 @@ jobs: # even if they are skipped due to no changes in the relevant files. required-checks: name: Required checks 🐍 - needs: [changes, check, serialization-schema] + needs: [changes, check, test, serialization-schema] if: ${{ !cancelled() }} runs-on: ubuntu-latest steps: @@ -176,7 +178,7 @@ jobs: echo "All required checks passed" coverage: - needs: [changes, check, build_binary] + needs: [changes, test] # Run only if there are changes in the relevant files and the check job passed or was skipped if: always() && !failure() && !cancelled() && needs.changes.outputs.python == 'true' && github.event_name != 'merge_group' runs-on: ubuntu-latest @@ -197,7 +199,9 @@ jobs: - name: Download the hugr binary uses: actions/download-artifact@v4 with: - name: hugr-binary + name: hugr_binary + path: ${{env.HUGR_BIN}} + - name: Run python tests with coverage instrumentation run: poetry run pytest --cov=./ --cov-report=xml From c1124f4ea1e331282f3768ec68e7f7d81d2c6c98 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 24 May 2024 14:03:00 +0100 Subject: [PATCH 32/57] chmod +x --- .github/workflows/ci-py.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index 16c672481..8a44e2b24 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -119,7 +119,9 @@ jobs: path: ${{env.HUGR_BIN}} - name: Run tests - run: poetry run pytest + run: | + chmod +x $HUGR_BIN + poetry run pytest # Ensure that the serialization schema is up to date serialization-schema: @@ -204,7 +206,9 @@ jobs: - name: Run python tests with coverage instrumentation - run: poetry run pytest --cov=./ --cov-report=xml + run: | + chmod +x $HUGR_BIN + poetry run pytest --cov=./ --cov-report=xml - name: Upload python coverage to codecov.io uses: codecov/codecov-action@v4 From 9485fef85f1c3b649a56c69509a2972654285c5e Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 24 May 2024 14:09:14 +0100 Subject: [PATCH 33/57] download binary bin folder --- .github/workflows/ci-py.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index 8a44e2b24..7edbc6a70 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -13,7 +13,7 @@ on: env: SCCACHE_GHA_ENABLED: "true" - HUGR_BIN: ${{ github.workspace }}/target/release/hugr + HUGR_BIN: ${{ github.workspace }}/bin/hugr jobs: # Check if changes were made to the relevant files. @@ -90,7 +90,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: hugr_binary - path: ${{env.HUGR_BIN}} + path: target/release/hugr test: needs: [changes, check, build_binary] if: ${{ needs.changes.outputs.python == 'true' }} @@ -116,12 +116,10 @@ jobs: uses: actions/download-artifact@v4 with: name: hugr_binary - path: ${{env.HUGR_BIN}} + path: bin - name: Run tests - run: | - chmod +x $HUGR_BIN - poetry run pytest + run: poetry run pytest # Ensure that the serialization schema is up to date serialization-schema: @@ -202,13 +200,11 @@ jobs: uses: actions/download-artifact@v4 with: name: hugr_binary - path: ${{env.HUGR_BIN}} + path: bin - name: Run python tests with coverage instrumentation - run: | - chmod +x $HUGR_BIN - poetry run pytest --cov=./ --cov-report=xml + run: poetry run pytest --cov=./ --cov-report=xml - name: Upload python coverage to codecov.io uses: codecov/codecov-action@v4 From 95d7639e46e04b61db2a595d8eb69fd9865cb8df Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 24 May 2024 15:14:40 +0100 Subject: [PATCH 34/57] direction enum --- hugr-py/src/hugr/hugr.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index bde6f3de4..8caeaf5a3 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -1,7 +1,8 @@ from dataclasses import dataclass, field, replace from collections.abc import Collection, Mapping -from typing import Iterable, Sequence, Protocol, Generic, TypeVar, overload +from enum import Enum +from typing import Iterable, Sequence, Protocol, Generic, TypeVar, overload, ClassVar from hugr.serialization.serial_hugr import SerialHugr from hugr.serialization.ops import BaseOp, OpType as SerialOp @@ -10,16 +11,17 @@ from hugr.utils import BiMap +class Direction(Enum): + INCOMING = 0 + OUTGOING = 1 + + @dataclass(frozen=True, eq=True, order=True) -class Port: +class InPort: node: "Node" offset: int sub_offset: int = 0 - - -@dataclass(frozen=True, eq=True, order=True) -class InPort(Port): - pass + direction: ClassVar[Direction] = Direction.INCOMING class ToPort(Protocol): @@ -27,7 +29,12 @@ def to_port(self) -> "OutPort": ... @dataclass(frozen=True, eq=True, order=True) -class OutPort(Port, ToPort): +class OutPort(ToPort): + node: "Node" + offset: int + sub_offset: int = 0 + direction: ClassVar[Direction] = Direction.OUTGOING + def to_port(self) -> "OutPort": return self @@ -198,6 +205,15 @@ def num_in_ports(self, node: Node) -> int: def num_out_ports(self, node: Node) -> int: return len(self.out_ports(node)) + def node_ports( + self, node: Node, direction: Direction + ) -> Collection[InPort] | Collection[OutPort]: + return ( + self.in_ports(node) + if direction == Direction.INCOMING + else self.out_ports(node) + ) + def in_ports(self, node: Node) -> Collection[InPort]: # can be optimised by caching number of ports # or by guaranteeing that all ports are contiguous From cc6719e2de69c925d04d2335ff4b7902b5c6cc20 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 24 May 2024 16:33:31 +0100 Subject: [PATCH 35/57] differentiate ports and connected links correctly --- hugr-py/src/hugr/hugr.py | 104 +++++++++++++++++++++++-------- hugr-py/tests/test_hugr_build.py | 19 ++++-- 2 files changed, 91 insertions(+), 32 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 8caeaf5a3..7e74fe0b4 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -1,8 +1,18 @@ from dataclasses import dataclass, field, replace -from collections.abc import Collection, Mapping +from collections.abc import Mapping from enum import Enum -from typing import Iterable, Sequence, Protocol, Generic, TypeVar, overload, ClassVar +from typing import ( + Iterable, + Self, + Sequence, + Protocol, + Generic, + TypeVar, + cast, + overload, + ClassVar, +) from hugr.serialization.serial_hugr import SerialHugr from hugr.serialization.ops import BaseOp, OpType as SerialOp @@ -17,10 +27,17 @@ class Direction(Enum): @dataclass(frozen=True, eq=True, order=True) -class InPort: +class _Port: node: "Node" offset: int - sub_offset: int = 0 + _sub_offset: int = 0 + + def next_sub_offset(self) -> Self: + return replace(self, _sub_offset=self._sub_offset + 1) + + +@dataclass(frozen=True, eq=True, order=True) +class InPort(_Port): direction: ClassVar[Direction] = Direction.INCOMING @@ -29,10 +46,7 @@ def to_port(self) -> "OutPort": ... @dataclass(frozen=True, eq=True, order=True) -class OutPort(ToPort): - node: "Node" - offset: int - sub_offset: int = 0 +class OutPort(_Port, ToPort): direction: ClassVar[Direction] = Direction.OUTGOING def to_port(self) -> "OutPort": @@ -81,6 +95,12 @@ def inp(self, offset: int) -> InPort: def out(self, offset: int) -> OutPort: return OutPort(self, offset) + def port(self, offset: int, direction: Direction) -> InPort | OutPort: + if direction == Direction.INCOMING: + return self.inp(offset) + else: + return self.out(offset) + class Op(Protocol): def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: ... @@ -108,6 +128,8 @@ def num_out(self) -> int | None: class NodeData: op: Op parent: Node | None + _num_inps: int = 0 + _num_outs: int = 0 # TODO children field? def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: @@ -118,6 +140,7 @@ def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: P = TypeVar("P", InPort, OutPort) +K = TypeVar("K", InPort, OutPort) def _unused_sub_offset(port: P, links: BiMap[OutPort, InPort]) -> P: @@ -128,7 +151,7 @@ def _unused_sub_offset(port: P, links: BiMap[OutPort, InPort]) -> P: case InPort(_): d = links.bck while port in d: - port = replace(port, sub_offset=port.sub_offset + 1) + port = port.next_sub_offset() return port @@ -190,37 +213,66 @@ def add_link(self, src: OutPort, dst: InPort) -> None: src = _unused_sub_offset(src, self._links) dst = _unused_sub_offset(dst, self._links) if self._links.get_left(dst) is not None: - dst = replace(dst, sub_offset=dst.sub_offset + 1) + dst = replace(dst, _sub_offset=dst._sub_offset + 1) self._links.insert_left(src, dst) + self[src.node]._num_outs = max(self[src.node]._num_outs, src.offset + 1) + self[dst.node]._num_inps = max(self[dst.node]._num_inps, dst.offset + 1) + def delete_link(self, src: OutPort, dst: InPort) -> None: self._links.delete_left(src) def num_nodes(self) -> int: return len(self._nodes) - len(self._free_nodes) + def num_ports(self, node: Node, direction: Direction) -> int: + return ( + self.num_in_ports(node) + if direction == Direction.INCOMING + else self.num_out_ports(node) + ) + def num_in_ports(self, node: Node) -> int: - return len(self.in_ports(node)) + return self[node]._num_inps def num_out_ports(self, node: Node) -> int: - return len(self.out_ports(node)) + return self[node]._num_outs - def node_ports( - self, node: Node, direction: Direction - ) -> Collection[InPort] | Collection[OutPort]: - return ( - self.in_ports(node) - if direction == Direction.INCOMING - else self.out_ports(node) - ) + def _linked_ports(self, port: P, links: dict[P, K]) -> Iterable[K]: + port = replace(port, _sub_offset=0) + while port in links: + # sub offset not used in API + yield replace(links[port], _sub_offset=0) + port = port.next_sub_offset() - def in_ports(self, node: Node) -> Collection[InPort]: - # can be optimised by caching number of ports - # or by guaranteeing that all ports are contiguous - return [p for p in self._links.bck if p.node == node] + # TODO: single linked port - def out_ports(self, node: Node) -> Collection[OutPort]: - return [p for p in self._links.fwd if p.node == node] + def _node_links(self, node: Node, links: dict[P, K]) -> Iterable[tuple[P, list[K]]]: + try: + direction = next(iter(links.keys())).direction + except StopIteration: + return iter(()) + # iterate over known offsets + for offset in range(self.num_ports(node, direction)): + port = cast(P, node.port(offset, direction)) + # if the 0 sub-offset is linked + yield port, list(self._linked_ports(port, links)) + + def outgoing_links(self, node: Node) -> Iterable[tuple[OutPort, list[InPort]]]: + return self._node_links(node, self._links.fwd) + + def incoming_links(self, node: Node) -> Iterable[tuple[InPort, list[OutPort]]]: + return self._node_links(node, self._links.bck) + + def num_incoming(self, node: Node) -> int: + # connecetd links + return sum(1 for _ in self.incoming_links(node)) + + def num_outgoing(self, node: Node) -> int: + # connecetd links + return sum(1 for _ in self.outgoing_links(node)) + + # TODO: num_links and _linked_ports def insert_hugr(self, hugr: "Hugr", parent: Node | None = None) -> dict[Node, Node]: mapping: dict[Node, Node] = {} diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index dcc7a57b9..b7529be30 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -86,8 +86,8 @@ def test_stable_indices(): h.add_link(nodes[0].out(0), nodes[1].inp(0)) - assert h.num_out_ports(nodes[0]) == 1 - assert h.num_in_ports(nodes[1]) == 1 + assert h.num_outgoing(nodes[0]) == 1 + assert h.num_incoming(nodes[1]) == 1 assert h.delete_node(nodes[1]) is not None assert h._nodes[nodes[1].idx] is None @@ -96,8 +96,8 @@ def test_stable_indices(): assert len(h._nodes) == 4 assert h._free_nodes == [nodes[1]] - assert h.num_out_ports(nodes[0]) == 0 - assert h.num_in_ports(nodes[1]) == 0 + assert h.num_outgoing(nodes[0]) == 0 + assert h.num_incoming(nodes[1]) == 0 with pytest.raises(KeyError): _ = h[nodes[1]] @@ -123,7 +123,15 @@ def test_multiport(): h = Dfg([BOOL_T], [BOOL_T] * 2) (a,) = h.inputs() h.set_outputs(a, a) - + in_n, ou_n = h.input_node, h.output_node + assert list(h.hugr.outgoing_links(in_n)) == [ + (in_n.out(0), [ou_n.inp(0), ou_n.inp(1)]), + ] + + assert list(h.hugr.incoming_links(ou_n)) == [ + (ou_n.inp(0), [in_n.out(0)]), + (ou_n.inp(1), [in_n.out(0)]), + ] _validate(h.hugr) @@ -160,7 +168,6 @@ def test_multi_out(): a, b = h.inputs() a, b = h.add(DivMod(a, b)) h.set_outputs(a, b) - _validate(h.hugr) From 891767baa37826fc2c2bd601341b50e1d28748a2 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 24 May 2024 16:35:59 +0100 Subject: [PATCH 36/57] Squashed commit of the following: commit 22ce1b7350408c2b2b17c9c7773869967488e393 Author: Seyon Sivarajah Date: Fri May 24 15:08:11 2024 +0100 fix paths commit 60407bb42e70ca86f9cd847a5f35a184b5f82a53 Author: Seyon Sivarajah Date: Fri May 24 14:32:36 2024 +0100 empty commit commit 53fd488826f3c17feb693cc2880e910c3441486b Author: Seyon Sivarajah Date: Fri May 24 14:23:02 2024 +0100 [REVERTME] add ss/pybuild to CI req --- .github/workflows/ci-py.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index 7edbc6a70..55440c39e 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -13,7 +13,8 @@ on: env: SCCACHE_GHA_ENABLED: "true" - HUGR_BIN: ${{ github.workspace }}/bin/hugr + HUGR_BIN_DIR: ${{ github.workspace }}/target/release + HUGR_BIN: ${{ github.workspace }}/target/release/hugr jobs: # Check if changes were made to the relevant files. @@ -116,7 +117,7 @@ jobs: uses: actions/download-artifact@v4 with: name: hugr_binary - path: bin + path: ${{env.HUGR_BIN_DIR}} - name: Run tests run: poetry run pytest @@ -200,7 +201,7 @@ jobs: uses: actions/download-artifact@v4 with: name: hugr_binary - path: bin + path: ${{env.HUGR_BIN_DIR}} - name: Run python tests with coverage instrumentation From bcb26697e095cdfa2af4bf4b131f95fff40fd03a Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 24 May 2024 16:41:04 +0100 Subject: [PATCH 37/57] import Self from typing extensions --- hugr-py/src/hugr/hugr.py | 3 ++- poetry.lock | 8 ++++---- pyproject.toml | 3 +++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 7e74fe0b4..b4f70050a 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -4,7 +4,6 @@ from enum import Enum from typing import ( Iterable, - Self, Sequence, Protocol, Generic, @@ -14,6 +13,8 @@ ClassVar, ) +from typing_extensions import Self + from hugr.serialization.serial_hugr import SerialHugr from hugr.serialization.ops import BaseOp, OpType as SerialOp import hugr.serialization.ops as sops diff --git a/poetry.lock b/poetry.lock index 24e529051..3112b9dd0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -589,13 +589,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, + {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, ] [[package]] @@ -621,4 +621,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "487ddc7148ab4801c38871d976104e32eb884e91342ea1cdc05d2aa090489a0c" +content-hash = "1238f0887779cfa07a16819569dcc29377cdf936b5e8c227fd5959250d1977f3" diff --git a/pyproject.toml b/pyproject.toml index 3138b5893..a67f62af5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,9 @@ toml = "^0.10.0" [tool.poetry.group.hugr.dependencies] hugr = { path = "hugr-py", develop = true } + +[tool.poetry.dependencies] +typing-extensions = "^4.12.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" From 3052c2db10cecdf19b9b4922977198c55ef85e29 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 24 May 2024 16:44:44 +0100 Subject: [PATCH 38/57] make mypy happy --- hugr-py/src/hugr/hugr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index b4f70050a..619a09955 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -252,7 +252,7 @@ def _node_links(self, node: Node, links: dict[P, K]) -> Iterable[tuple[P, list[K try: direction = next(iter(links.keys())).direction except StopIteration: - return iter(()) + return # iterate over known offsets for offset in range(self.num_ports(node, direction)): port = cast(P, node.port(offset, direction)) From df796c740b1f13af2401aca286022fdaad2a4810 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 24 May 2024 16:49:38 +0100 Subject: [PATCH 39/57] chmod +x --- .github/workflows/ci-py.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index 55440c39e..735cba0e6 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -120,7 +120,9 @@ jobs: path: ${{env.HUGR_BIN_DIR}} - name: Run tests - run: poetry run pytest + run: | + chmod +x $HUGR_BIN + poetry run pytest # Ensure that the serialization schema is up to date serialization-schema: @@ -205,7 +207,9 @@ jobs: - name: Run python tests with coverage instrumentation - run: poetry run pytest --cov=./ --cov-report=xml + run: | + chmod +x $HUGR_BIN + poetry run pytest --cov=./ --cov-report=xml - name: Upload python coverage to codecov.io uses: codecov/codecov-action@v4 From 53cca4a891cbe307ac5bc053e2dca7d499169ecc Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 28 May 2024 10:27:18 +0100 Subject: [PATCH 40/57] fix: bimap existing key overwrite --- hugr-py/src/hugr/utils.py | 7 +++++-- hugr-py/tests/test_bimap.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/hugr-py/src/hugr/utils.py b/hugr-py/src/hugr/utils.py index f7d9fef25..c4bd21124 100644 --- a/hugr-py/src/hugr/utils.py +++ b/hugr-py/src/hugr/utils.py @@ -37,12 +37,15 @@ def get_right(self, key: L) -> R | None: return self.fwd.get(key) def insert_left(self, key: L, value: R) -> None: + if (existing_key := self.bck.get(value)) is not None: + del self.fwd[existing_key] + if (existing_value := self.fwd.get(key)) is not None: + del self.bck[existing_value] self.fwd[key] = value self.bck[value] = key def insert_right(self, key: R, value: L) -> None: - self.bck[key] = value - self.fwd[value] = key + self.insert_left(value, key) def delete_left(self, key: L) -> None: del self.bck[self.fwd[key]] diff --git a/hugr-py/tests/test_bimap.py b/hugr-py/tests/test_bimap.py index bd17835eb..028498c9c 100644 --- a/hugr-py/tests/test_bimap.py +++ b/hugr-py/tests/test_bimap.py @@ -50,3 +50,14 @@ def test_len() -> None: bimap.delete_left("a") assert len(bimap) == 1 + + +def test_existing_key() -> None: + bimap: BiMap[str, int] = BiMap() + bimap.insert_left("a", 1) + bimap.insert_left("b", 1) + + assert bimap.get_right("b") == 1 + assert bimap.get_left(1) == "b" + + assert bimap.get_right("a") is None From 9802c5816b7969a6b68e4cd843d36ae4dd8ef826 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 28 May 2024 10:35:11 +0100 Subject: [PATCH 41/57] minor comments --- hugr-py/src/hugr/hugr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 619a09955..364d4e26f 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -221,6 +221,7 @@ def add_link(self, src: OutPort, dst: InPort) -> None: self[dst.node]._num_inps = max(self[dst.node]._num_inps, dst.offset + 1) def delete_link(self, src: OutPort, dst: InPort) -> None: + # TODO make sure sub-offset is handled correctly self._links.delete_left(src) def num_nodes(self) -> int: @@ -256,7 +257,6 @@ def _node_links(self, node: Node, links: dict[P, K]) -> Iterable[tuple[P, list[K # iterate over known offsets for offset in range(self.num_ports(node, direction)): port = cast(P, node.port(offset, direction)) - # if the 0 sub-offset is linked yield port, list(self._linked_ports(port, links)) def outgoing_links(self, node: Node) -> Iterable[tuple[OutPort, list[InPort]]]: From 329f13acbe22f0150d684788fae628cad82f88f8 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 28 May 2024 10:59:01 +0100 Subject: [PATCH 42/57] separate sub ports in to internal type --- hugr-py/src/hugr/hugr.py | 90 ++++++++++++++++++++++---------- hugr-py/tests/test_hugr_build.py | 7 +++ 2 files changed, 69 insertions(+), 28 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 364d4e26f..126fcca17 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -31,10 +31,6 @@ class Direction(Enum): class _Port: node: "Node" offset: int - _sub_offset: int = 0 - - def next_sub_offset(self) -> Self: - return replace(self, _sub_offset=self._sub_offset + 1) @dataclass(frozen=True, eq=True, order=True) @@ -144,23 +140,36 @@ def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: K = TypeVar("K", InPort, OutPort) -def _unused_sub_offset(port: P, links: BiMap[OutPort, InPort]) -> P: - d: dict[OutPort, InPort] | dict[InPort, OutPort] - match port: +@dataclass(frozen=True, eq=True, order=True) +class _SubPort(Generic[P]): + port: P + sub_offset: int = 0 + + def next_sub_offset(self) -> Self: + return replace(self, sub_offset=self.sub_offset + 1) + + +_SO = _SubPort[OutPort] +_SI = _SubPort[InPort] + + +def _unused_sub_offset(sub_port: _SubPort[P], links: BiMap[_SO, _SI]) -> _SubPort[P]: + d: dict[_SO, _SI] | dict[_SI, _SO] + match sub_port.port: case OutPort(_): d = links.fwd case InPort(_): d = links.bck - while port in d: - port = port.next_sub_offset() - return port + while sub_port in d: + sub_port = sub_port.next_sub_offset() + return sub_port @dataclass() class Hugr(Mapping[Node, NodeData]): root: Node _nodes: list[NodeData | None] - _links: BiMap[OutPort, InPort] + _links: BiMap[_SO, _SI] _free_nodes: list[Node] def __init__(self, root_op: Op) -> None: @@ -202,27 +211,33 @@ def add_node( def delete_node(self, node: Node) -> NodeData | None: for offset in range(self.num_in_ports(node)): - self._links.delete_right(node.inp(offset)) + self._links.delete_right(_SubPort(node.inp(offset))) for offset in range(self.num_out_ports(node)): - self._links.delete_left(node.out(offset)) + self._links.delete_left(_SubPort(node.out(offset))) weight, self._nodes[node.idx] = self._nodes[node.idx], None self._free_nodes.append(node) return weight def add_link(self, src: OutPort, dst: InPort) -> None: - src = _unused_sub_offset(src, self._links) - dst = _unused_sub_offset(dst, self._links) - if self._links.get_left(dst) is not None: - dst = replace(dst, _sub_offset=dst._sub_offset + 1) - self._links.insert_left(src, dst) + src_sub = _unused_sub_offset(_SubPort(src), self._links) + dst_sub = _unused_sub_offset(_SubPort(dst), self._links) + # if self._links.get_left(dst_sub) is not None: + # dst = replace(dst, _sub_offset=dst._sub_offset + 1) + self._links.insert_left(src_sub, dst_sub) self[src.node]._num_outs = max(self[src.node]._num_outs, src.offset + 1) self[dst.node]._num_inps = max(self[dst.node]._num_inps, dst.offset + 1) def delete_link(self, src: OutPort, dst: InPort) -> None: + try: + sub_offset = next( + i for i, inp in enumerate(self.linked_ports(src)) if inp == dst + ) + self._links.delete_left(_SubPort(src, sub_offset)) + except StopIteration: + return # TODO make sure sub-offset is handled correctly - self._links.delete_left(src) def num_nodes(self) -> int: return len(self._nodes) - len(self._free_nodes) @@ -240,18 +255,33 @@ def num_in_ports(self, node: Node) -> int: def num_out_ports(self, node: Node) -> int: return self[node]._num_outs - def _linked_ports(self, port: P, links: dict[P, K]) -> Iterable[K]: - port = replace(port, _sub_offset=0) - while port in links: + def _linked_ports( + self, port: P, links: dict[_SubPort[P], _SubPort[K]] + ) -> Iterable[K]: + sub_port = _SubPort(port) + while sub_port in links: # sub offset not used in API - yield replace(links[port], _sub_offset=0) - port = port.next_sub_offset() + yield links[sub_port].port + sub_port = sub_port.next_sub_offset() + + @overload + def linked_ports(self, port: OutPort) -> Iterable[InPort]: ... + @overload + def linked_ports(self, port: InPort) -> Iterable[OutPort]: ... + def linked_ports(self, port: OutPort | InPort): + match port: + case OutPort(_): + return self._linked_ports(port, self._links.fwd) + case InPort(_): + return self._linked_ports(port, self._links.bck) # TODO: single linked port - def _node_links(self, node: Node, links: dict[P, K]) -> Iterable[tuple[P, list[K]]]: + def _node_links( + self, node: Node, links: dict[_SubPort[P], _SubPort[K]] + ) -> Iterable[tuple[P, list[K]]]: try: - direction = next(iter(links.keys())).direction + direction = next(iter(links.keys())).port.direction except StopIteration: return # iterate over known offsets @@ -289,7 +319,8 @@ def insert_hugr(self, hugr: "Hugr", parent: Node | None = None) -> dict[Node, No for src, dst in hugr._links.items(): self.add_link( - mapping[src.node].out(src.offset), mapping[dst.node].inp(dst.offset) + mapping[src.port.node].out(src.port.offset), + mapping[dst.port.node].inp(dst.port.offset), ) return mapping @@ -300,7 +331,10 @@ def to_serial(self) -> SerialHugr: # non contiguous indices will be erased nodes=[node.to_serial(Node(idx), self) for idx, node in enumerate(node_it)], edges=[ - ((src.node.idx, src.offset), (dst.node.idx, dst.offset)) + ( + (src.port.node.idx, src.port.offset), + (dst.port.node.idx, dst.port.offset), + ) for src, dst in self._links.items() ], ) diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index b7529be30..858d0119d 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -132,6 +132,13 @@ def test_multiport(): (ou_n.inp(0), [in_n.out(0)]), (ou_n.inp(1), [in_n.out(0)]), ] + + assert list(h.hugr.linked_ports(in_n.out(0))) == [ + ou_n.inp(0), + ou_n.inp(1), + ] + + assert list(h.hugr.linked_ports(ou_n.inp(0))) == [in_n.out(0)] _validate(h.hugr) From dd9e3514eb2e6a65d5bed6860609fdee94a73a99 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 28 May 2024 11:53:24 +0100 Subject: [PATCH 43/57] rename ToPort to Wire --- hugr-py/src/hugr/hugr.py | 48 ++++++++++++++++---------------- hugr-py/tests/test_hugr_build.py | 12 ++++---- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 126fcca17..03731a665 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -1,5 +1,5 @@ +from __future__ import annotations from dataclasses import dataclass, field, replace - from collections.abc import Mapping from enum import Enum from typing import ( @@ -29,7 +29,7 @@ class Direction(Enum): @dataclass(frozen=True, eq=True, order=True) class _Port: - node: "Node" + node: Node offset: int @@ -38,20 +38,20 @@ class InPort(_Port): direction: ClassVar[Direction] = Direction.INCOMING -class ToPort(Protocol): - def to_port(self) -> "OutPort": ... +class Wire(Protocol): + def out_port(self) -> OutPort: ... @dataclass(frozen=True, eq=True, order=True) -class OutPort(_Port, ToPort): +class OutPort(_Port, Wire): direction: ClassVar[Direction] = Direction.OUTGOING - def to_port(self) -> "OutPort": + def out_port(self) -> OutPort: return self @dataclass(frozen=True, eq=True, order=True) -class Node(ToPort): +class Node(Wire): idx: int _num_out_ports: int | None = field(default=None, compare=False) @@ -83,7 +83,7 @@ def __getitem__( case tuple(xs): return [self[i] for i in xs] - def to_port(self) -> "OutPort": + def out_port(self) -> "OutPort": return OutPort(self, 0) def inp(self, offset: int) -> InPort: @@ -100,7 +100,7 @@ def port(self, offset: int, direction: Direction) -> InPort | OutPort: class Op(Protocol): - def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: ... + def to_serial(self, node: Node, hugr: Hugr) -> SerialOp: ... T = TypeVar("T", bound=BaseOp) @@ -110,13 +110,13 @@ def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: ... class DummyOp(Op, Generic[T]): _serial_op: T - def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: + def to_serial(self, node: Node, hugr: Hugr) -> SerialOp: return SerialOp(root=self._serial_op) # type: ignore class Command(Protocol): def op(self) -> Op: ... - def incoming(self) -> Iterable[ToPort]: ... + def incoming(self) -> Iterable[Wire]: ... def num_out(self) -> int | None: return None @@ -129,7 +129,7 @@ class NodeData: _num_outs: int = 0 # TODO children field? - def to_serial(self, node: Node, hugr: "Hugr") -> SerialOp: + def to_serial(self, node: Node, hugr: Hugr) -> SerialOp: o = self.op.to_serial(node, hugr) o.root.parent = self.parent.idx if self.parent else node.idx @@ -305,7 +305,7 @@ def num_outgoing(self, node: Node) -> int: # TODO: num_links and _linked_ports - def insert_hugr(self, hugr: "Hugr", parent: Node | None = None) -> dict[Node, Node]: + def insert_hugr(self, hugr: Hugr, parent: Node | None = None) -> dict[Node, Node]: mapping: dict[Node, Node] = {} for idx, node_data in enumerate(hugr._nodes): @@ -340,7 +340,7 @@ def to_serial(self) -> SerialHugr: ) @classmethod - def from_serial(cls, serial: SerialHugr) -> "Hugr": + def from_serial(cls, serial: SerialHugr) -> Hugr: raise NotImplementedError @@ -371,7 +371,7 @@ def __init__( ) @classmethod - def endo(cls, types: Sequence[Type]) -> "Dfg": + def endo(cls, types: Sequence[Type]) -> Dfg: return Dfg(types, types) def _input_op(self) -> DummyOp[sops.Input]: @@ -386,7 +386,7 @@ def inputs(self) -> list[OutPort]: for i in range(len(self._input_op()._serial_op.types)) ] - def add_op(self, op: Op, /, *args: ToPort, num_outs: int | None = None) -> Node: + def add_op(self, op: Op, /, *args: Wire, num_outs: int | None = None) -> Node: new_n = self.hugr.add_node(op, self.root, num_outs=num_outs) self._wire_up(new_n, args) return new_n @@ -394,7 +394,7 @@ def add_op(self, op: Op, /, *args: ToPort, num_outs: int | None = None) -> Node: def add(self, com: Command) -> Node: return self.add_op(com.op(), *com.incoming(), num_outs=com.num_out()) - def insert_nested(self, dfg: "Dfg", *args: ToPort) -> Node: + def insert_nested(self, dfg: Dfg, *args: Wire) -> Node: mapping = self.hugr.insert_hugr(dfg.hugr, self.root) self._wire_up(mapping[dfg.root], args) return mapping[dfg.root] @@ -403,8 +403,8 @@ def add_nested( self, input_types: Sequence[Type], output_types: Sequence[Type], - ports: Iterable[ToPort], - ) -> "Dfg": + ports: Iterable[Wire], + ) -> Dfg: dfg = Dfg(input_types, output_types) mapping = self.hugr.insert_hugr(dfg.hugr, self.root) self._wire_up(mapping[dfg.root], ports) @@ -415,21 +415,21 @@ def add_nested( return dfg - def set_outputs(self, *args: ToPort) -> None: + def set_outputs(self, *args: Wire) -> None: self._wire_up(self.output_node, args) - def make_tuple(self, tys: Sequence[Type], *args: ToPort) -> Node: + def make_tuple(self, tys: Sequence[Type], *args: Wire) -> Node: ports = list(args) assert len(tys) == len(ports), "Number of types must match number of ports" return self.add_op(DummyOp(sops.MakeTuple(parent=0, tys=list(tys))), *args) - def split_tuple(self, tys: Sequence[Type], port: ToPort) -> list[OutPort]: + def split_tuple(self, tys: Sequence[Type], port: Wire) -> list[OutPort]: tys = list(tys) n = self.add_op(DummyOp(sops.UnpackTuple(parent=0, tys=tys)), port) return [n.out(i) for i in range(len(tys))] - def _wire_up(self, node: Node, ports: Iterable[ToPort]): + def _wire_up(self, node: Node, ports: Iterable[Wire]): for i, p in enumerate(ports): - src = p.to_port() + src = p.out_port() self.hugr.add_link(src, node.inp(i)) diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index 858d0119d..3b3bd719b 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -2,7 +2,7 @@ import subprocess import os import pathlib -from hugr.hugr import Dfg, Hugr, DummyOp, Node, Command, ToPort, Op +from hugr.hugr import Dfg, Hugr, DummyOp, Node, Command, Wire, Op import hugr.serialization.tys as stys import hugr.serialization.ops as sops import pytest @@ -32,9 +32,9 @@ @dataclass class Not(Command): - a: ToPort + a: Wire - def incoming(self) -> list[ToPort]: + def incoming(self) -> list[Wire]: return [self.a] def num_out(self) -> int | None: @@ -46,10 +46,10 @@ def op(self) -> Op: @dataclass class DivMod(Command): - a: ToPort - b: ToPort + a: Wire + b: Wire - def incoming(self) -> list[ToPort]: + def incoming(self) -> list[Wire]: return [self.a, self.b] def num_out(self) -> int | None: From 0d55959dbc9a976b000221159089eaee9d9fb477 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 28 May 2024 12:45:42 +0100 Subject: [PATCH 44/57] feat: inter graph + state order edge --- hugr-py/src/hugr/hugr.py | 39 +++++++++++++++++++------------- hugr-py/tests/test_hugr_build.py | 17 +++++++++++++- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 03731a665..e734bc56b 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -153,18 +153,6 @@ def next_sub_offset(self) -> Self: _SI = _SubPort[InPort] -def _unused_sub_offset(sub_port: _SubPort[P], links: BiMap[_SO, _SI]) -> _SubPort[P]: - d: dict[_SO, _SI] | dict[_SI, _SO] - match sub_port.port: - case OutPort(_): - d = links.fwd - case InPort(_): - d = links.bck - while sub_port in d: - sub_port = sub_port.next_sub_offset() - return sub_port - - @dataclass() class Hugr(Mapping[Node, NodeData]): root: Node @@ -219,9 +207,21 @@ def delete_node(self, node: Node) -> NodeData | None: self._free_nodes.append(node) return weight + def _unused_sub_offset(self, port: P) -> _SubPort[P]: + d: dict[_SO, _SI] | dict[_SI, _SO] + match port: + case OutPort(_): + d = self._links.fwd + case InPort(_): + d = self._links.bck + sub_port = _SubPort(port) + while sub_port in d: + sub_port = sub_port.next_sub_offset() + return sub_port + def add_link(self, src: OutPort, dst: InPort) -> None: - src_sub = _unused_sub_offset(_SubPort(src), self._links) - dst_sub = _unused_sub_offset(_SubPort(dst), self._links) + src_sub = self._unused_sub_offset(src) + dst_sub = self._unused_sub_offset(dst) # if self._links.get_left(dst_sub) is not None: # dst = replace(dst, _sub_offset=dst._sub_offset + 1) self._links.insert_left(src_sub, dst_sub) @@ -403,11 +403,11 @@ def add_nested( self, input_types: Sequence[Type], output_types: Sequence[Type], - ports: Iterable[Wire], + *args: Wire, ) -> Dfg: dfg = Dfg(input_types, output_types) mapping = self.hugr.insert_hugr(dfg.hugr, self.root) - self._wire_up(mapping[dfg.root], ports) + self._wire_up(mapping[dfg.root], args) dfg.hugr = self.hugr dfg.input_node = mapping[dfg.input_node] dfg.output_node = mapping[dfg.output_node] @@ -429,6 +429,13 @@ def split_tuple(self, tys: Sequence[Type], port: Wire) -> list[OutPort]: return [n.out(i) for i in range(len(tys))] + def add_state_order(self, src: Node, dst: Node) -> None: + # adds edge to the right of all existing edges + # breaks if further edges are added + self.hugr.add_link( + src.out(self.hugr.num_outgoing(src)), dst.inp(self.hugr.num_incoming(dst)) + ) + def _wire_up(self, node: Node, ports: Iterable[Wire]): for i, p in enumerate(ports): src = p.out_port() diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index 3b3bd719b..2b86a809b 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -213,10 +213,25 @@ def _nested_nop(dfg: Dfg): h = Dfg.endo([BOOL_T]) (a,) = h.inputs() - nested = h.add_nested([BOOL_T], [BOOL_T], [a]) + nested = h.add_nested([BOOL_T], [BOOL_T], a) _nested_nop(nested) h.set_outputs(nested.root) _validate(h.hugr) + + +def test_build_inter_graph(): + h = Dfg.endo([BOOL_T]) + (a,) = h.inputs() + nested = h.add_nested([], [BOOL_T]) + + nt = nested.add(Not(a)) + nested.set_outputs(nt) + # TODO a context manager could add this state order edge on + # exit by tracking parents of source nodes + h.add_state_order(h.input_node, nested.root) + h.set_outputs(nested.root) + + _validate(h.hugr) From 5ba5ad948799745e7c52f4d448badbb19705a67f Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Tue, 28 May 2024 13:08:48 +0100 Subject: [PATCH 45/57] put `add_dfg` in hugr --- hugr-py/src/hugr/hugr.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index e734bc56b..00710394d 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -324,6 +324,15 @@ def insert_hugr(self, hugr: Hugr, parent: Node | None = None) -> dict[Node, Node ) return mapping + def add_dfg(self, input_types: Sequence[Type], output_types: Sequence[Type]) -> Dfg: + dfg = Dfg(input_types, output_types) + mapping = self.insert_hugr(dfg.hugr, self.root) + dfg.hugr = self + dfg.input_node = mapping[dfg.input_node] + dfg.output_node = mapping[dfg.output_node] + dfg.root = mapping[dfg.root] + return dfg + def to_serial(self) -> SerialHugr: node_it = (node for node in self._nodes if node is not None) return SerialHugr( @@ -405,14 +414,8 @@ def add_nested( output_types: Sequence[Type], *args: Wire, ) -> Dfg: - dfg = Dfg(input_types, output_types) - mapping = self.hugr.insert_hugr(dfg.hugr, self.root) - self._wire_up(mapping[dfg.root], args) - dfg.hugr = self.hugr - dfg.input_node = mapping[dfg.input_node] - dfg.output_node = mapping[dfg.output_node] - dfg.root = mapping[dfg.root] - + dfg = self.hugr.add_dfg(input_types, output_types) + self._wire_up(dfg.root, args) return dfg def set_outputs(self, *args: Wire) -> None: From 9ebab76538c7d62d2e0c206b6331b0c7cb9b1fc4 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 29 May 2024 13:21:48 +0100 Subject: [PATCH 46/57] return `Iterator` from `getitem` --- hugr-py/src/hugr/hugr.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 00710394d..8fa796048 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -4,6 +4,7 @@ from enum import Enum from typing import ( Iterable, + Iterator, Sequence, Protocol, Generic, @@ -58,13 +59,13 @@ class Node(Wire): @overload def __getitem__(self, index: int) -> OutPort: ... @overload - def __getitem__(self, index: slice) -> Iterable[OutPort]: ... + def __getitem__(self, index: slice) -> Iterator[OutPort]: ... @overload - def __getitem__(self, index: tuple[int, ...]) -> list[OutPort]: ... + def __getitem__(self, index: tuple[int, ...]) -> Iterator[OutPort]: ... def __getitem__( self, index: int | slice | tuple[int, ...] - ) -> OutPort | Iterable[OutPort]: + ) -> OutPort | Iterator[OutPort]: match index: case int(index): if self._num_out_ports is not None: @@ -81,7 +82,7 @@ def __getitem__( step = index.step or 1 return (self[i] for i in range(start, stop, step)) case tuple(xs): - return [self[i] for i in xs] + return (self[i] for i in xs) def out_port(self) -> "OutPort": return OutPort(self, 0) From 27014bae37747cac40211dc0ad4a1ba48f41e6d0 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 29 May 2024 13:26:33 +0100 Subject: [PATCH 47/57] remove special tuple methods --- hugr-py/src/hugr/hugr.py | 11 ---------- hugr-py/tests/test_hugr_build.py | 35 ++++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 8fa796048..198d3bd89 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -422,17 +422,6 @@ def add_nested( def set_outputs(self, *args: Wire) -> None: self._wire_up(self.output_node, args) - def make_tuple(self, tys: Sequence[Type], *args: Wire) -> Node: - ports = list(args) - assert len(tys) == len(ports), "Number of types must match number of ports" - return self.add_op(DummyOp(sops.MakeTuple(parent=0, tys=list(tys))), *args) - - def split_tuple(self, tys: Sequence[Type], port: Wire) -> list[OutPort]: - tys = list(tys) - n = self.add_op(DummyOp(sops.UnpackTuple(parent=0, tys=tys)), port) - - return [n.out(i) for i in range(len(tys))] - def add_state_order(self, src: Node, dst: Node) -> None: # adds edge to the right of all existing edges # breaks if further edges are added diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index 2b86a809b..54ee00812 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -1,3 +1,4 @@ +from __future__ import annotations from dataclasses import dataclass import subprocess import os @@ -67,6 +68,36 @@ def op(self) -> Op: ) +@dataclass +class MakeTuple(Command): + types: list[stys.Type] + wires: list[Wire] + + def incoming(self) -> list[Wire]: + return self.wires + + def num_out(self) -> int | None: + return 1 + + def op(self) -> Op: + return DummyOp(sops.MakeTuple(parent=-1, tys=self.types)) + + +@dataclass +class UnpackTuple(Command): + types: list[stys.Type] + wire: Wire + + def incoming(self) -> list[Wire]: + return [self.wire] + + def num_out(self) -> int | None: + return len(self.types) + + def op(self) -> Op: + return DummyOp(sops.UnpackTuple(parent=-1, tys=self.types)) + + def _validate(h: Hugr, mermaid: bool = False): workspace_dir = pathlib.Path(__file__).parent.parent.parent # use the HUGR_BIN environment variable if set, otherwise use the debug build @@ -155,8 +186,8 @@ def test_tuple(): row = [BOOL_T, QB_T] h = Dfg.endo(row) a, b = h.inputs() - t = h.make_tuple(row, a, b) - a, b = h.split_tuple(row, t) + t = h.add(MakeTuple(row, [a, b])) + a, b = h.add(UnpackTuple(row, t)) h.set_outputs(a, b) _validate(h.hugr) From f02a0b86a3dee82cbe17d4f517a847028250a14f Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 29 May 2024 13:34:24 +0100 Subject: [PATCH 48/57] copy model when serialising --- hugr-py/src/hugr/hugr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 198d3bd89..2c7d87124 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -112,7 +112,7 @@ class DummyOp(Op, Generic[T]): _serial_op: T def to_serial(self, node: Node, hugr: Hugr) -> SerialOp: - return SerialOp(root=self._serial_op) # type: ignore + return SerialOp(root=self._serial_op.model_copy()) # type: ignore class Command(Protocol): From 1862c4e4376b319deb325592c6857e9203cf68f8 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 29 May 2024 14:26:15 +0100 Subject: [PATCH 49/57] implement `from_serial` --- hugr-py/src/hugr/hugr.py | 31 ++++++++++++++++++++++++++++++- hugr-py/tests/test_hugr_build.py | 11 +++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/hugr.py index 2c7d87124..1cdffd808 100644 --- a/hugr-py/src/hugr/hugr.py +++ b/hugr-py/src/hugr/hugr.py @@ -103,6 +103,9 @@ def port(self, offset: int, direction: Direction) -> InPort | OutPort: class Op(Protocol): def to_serial(self, node: Node, hugr: Hugr) -> SerialOp: ... + @classmethod + def from_serial(cls, serial: SerialOp) -> Self: ... + T = TypeVar("T", bound=BaseOp) @@ -114,6 +117,10 @@ class DummyOp(Op, Generic[T]): def to_serial(self, node: Node, hugr: Hugr) -> SerialOp: return SerialOp(root=self._serial_op.model_copy()) # type: ignore + @classmethod + def from_serial(cls, serial: SerialOp) -> DummyOp: + return DummyOp(serial.root) + class Command(Protocol): def op(self) -> Op: ... @@ -351,7 +358,29 @@ def to_serial(self) -> SerialHugr: @classmethod def from_serial(cls, serial: SerialHugr) -> Hugr: - raise NotImplementedError + assert serial.nodes, "Empty Hugr is invalid" + + hugr = Hugr.__new__(Hugr) + hugr._nodes = [] + hugr._links = BiMap() + hugr._free_nodes = [] + hugr.root = Node(0) + for idx, serial_node in enumerate(serial.nodes): + parent: Node | None = Node(serial_node.root.parent) + if serial_node.root.parent == idx: + hugr.root = Node(idx) + parent = None + serial_node.root.parent = -1 + hugr._nodes.append(NodeData(DummyOp.from_serial(serial_node), parent)) + + for (src_node, src_offset), (dst_node, dst_offset) in serial.edges: + if src_offset is None or dst_offset is None: + continue + hugr.add_link( + Node(src_node).out(src_offset), Node(dst_node).inp(dst_offset) + ) + + return hugr @dataclass() diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index 54ee00812..830e5734e 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -4,9 +4,11 @@ import os import pathlib from hugr.hugr import Dfg, Hugr, DummyOp, Node, Command, Wire, Op +from hugr.serialization import SerialHugr import hugr.serialization.tys as stys import hugr.serialization.ops as sops import pytest +import json BOOL_T = stys.Type(stys.SumType(stys.UnitSum(size=2))) QB_T = stys.Type(stys.Qubit()) @@ -98,7 +100,7 @@ def op(self) -> Op: return DummyOp(sops.UnpackTuple(parent=-1, tys=self.types)) -def _validate(h: Hugr, mermaid: bool = False): +def _validate(h: Hugr, mermaid: bool = False, roundtrip: bool = True): workspace_dir = pathlib.Path(__file__).parent.parent.parent # use the HUGR_BIN environment variable if set, otherwise use the debug build bin_loc = os.environ.get("HUGR_BIN", str(workspace_dir / "target/debug/hugr")) @@ -106,7 +108,12 @@ def _validate(h: Hugr, mermaid: bool = False): if mermaid: cmd.append("--mermaid") - subprocess.run(cmd, check=True, input=h.to_serial().to_json().encode()) + serial = h.to_serial().to_json() + subprocess.run(cmd, check=True, input=serial.encode()) + + if roundtrip: + h2 = Hugr.from_serial(SerialHugr.load_json(json.loads(serial))) + assert serial == h2.to_serial().to_json() def test_stable_indices(): From 14f59339733008333e1c393b7d1141293f80aac7 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 29 May 2024 14:29:19 +0100 Subject: [PATCH 50/57] fixup binary building (feature name changed) --- .github/workflows/ci-py.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index 735cba0e6..004c652fd 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -13,8 +13,8 @@ on: env: SCCACHE_GHA_ENABLED: "true" - HUGR_BIN_DIR: ${{ github.workspace }}/target/release - HUGR_BIN: ${{ github.workspace }}/target/release/hugr + HUGR_BIN_DIR: ${{ github.workspace }}/target/debug + HUGR_BIN: ${{ github.workspace }}/target/debug/hugr jobs: # Check if changes were made to the relevant files. @@ -86,7 +86,7 @@ jobs: - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable - name: Build HUGR binary - run: cargo build --release --features cli --bin hugr + run: cargo build --features _cli --bin hugr - name: Upload the binary to the artifacts uses: actions/upload-artifact@v4 with: From cecebc13c912bb72b5ac7e813f43cedb46576b5f Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 29 May 2024 16:39:06 +0100 Subject: [PATCH 51/57] build cli package --- .github/workflows/ci-py.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index 004c652fd..9cabbff04 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -86,12 +86,12 @@ jobs: - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable - name: Build HUGR binary - run: cargo build --features _cli --bin hugr + run: cargo build -p hugr-cli - name: Upload the binary to the artifacts uses: actions/upload-artifact@v4 with: name: hugr_binary - path: target/release/hugr + path: target/debug/hugr test: needs: [changes, check, build_binary] if: ${{ needs.changes.outputs.python == 'true' }} From b8a2642e0422b3dc8d897635a34afe3a33d445c8 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 29 May 2024 16:49:29 +0100 Subject: [PATCH 52/57] revert CI to main --- .github/workflows/ci-py.yml | 69 +++---------------------------------- 1 file changed, 4 insertions(+), 65 deletions(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index 9cabbff04..dfff0d309 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -13,8 +13,6 @@ on: env: SCCACHE_GHA_ENABLED: "true" - HUGR_BIN_DIR: ${{ github.workspace }}/target/debug - HUGR_BIN: ${{ github.workspace }}/target/debug/hugr jobs: # Check if changes were made to the relevant files. @@ -70,59 +68,8 @@ jobs: - name: Lint with ruff run: poetry run ruff check - build_binary: - needs: changes - if: ${{ needs.changes.outputs.python == 'true' }} - - name: Build HUGR binary - runs-on: ubuntu-latest - env: - SCCACHE_GHA_ENABLED: "true" - RUSTC_WRAPPER: "sccache" - - steps: - - uses: actions/checkout@v4 - - uses: mozilla-actions/sccache-action@v0.0.4 - - name: Install stable toolchain - uses: dtolnay/rust-toolchain@stable - - name: Build HUGR binary - run: cargo build -p hugr-cli - - name: Upload the binary to the artifacts - uses: actions/upload-artifact@v4 - with: - name: hugr_binary - path: target/debug/hugr - test: - needs: [changes, check, build_binary] - if: ${{ needs.changes.outputs.python == 'true' }} - name: check python ${{ matrix.python-version }} - runs-on: ubuntu-latest - - strategy: - matrix: - python-version: ['3.10', '3.12'] - steps: - - uses: actions/checkout@v4 - - name: Install poetry - run: pipx install poetry - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: "poetry" - - - name: Install the project libraries - run: poetry install - - name: Download the hugr binary - uses: actions/download-artifact@v4 - with: - name: hugr_binary - path: ${{env.HUGR_BIN_DIR}} - - name: Run tests - run: | - chmod +x $HUGR_BIN - poetry run pytest + run: poetry run pytest # Ensure that the serialization schema is up to date serialization-schema: @@ -159,7 +106,7 @@ jobs: # even if they are skipped due to no changes in the relevant files. required-checks: name: Required checks 🐍 - needs: [changes, check, test, serialization-schema] + needs: [changes, check, serialization-schema] if: ${{ !cancelled() }} runs-on: ubuntu-latest steps: @@ -181,7 +128,7 @@ jobs: echo "All required checks passed" coverage: - needs: [changes, test] + needs: [changes, check] # Run only if there are changes in the relevant files and the check job passed or was skipped if: always() && !failure() && !cancelled() && needs.changes.outputs.python == 'true' && github.event_name != 'merge_group' runs-on: ubuntu-latest @@ -199,17 +146,9 @@ jobs: - name: Install the project libraries run: poetry install - - name: Download the hugr binary - uses: actions/download-artifact@v4 - with: - name: hugr_binary - path: ${{env.HUGR_BIN_DIR}} - - name: Run python tests with coverage instrumentation - run: | - chmod +x $HUGR_BIN - poetry run pytest --cov=./ --cov-report=xml + run: poetry run pytest --cov=./ --cov-report=xml - name: Upload python coverage to codecov.io uses: codecov/codecov-action@v4 From de9d577d3c31db4b3a4f373634f075c9680688bc Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 29 May 2024 16:57:42 +0100 Subject: [PATCH 53/57] [REVERTME] enable CI on PRs in to this branch so it can be a feature branch --- .github/workflows/ci-py.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index dfff0d309..187d69f2e 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - main + - ss2165/pybuild merge_group: types: [checks_requested] workflow_dispatch: {} From b751e0db9a3ddb6ae2ee7a6b7689f3fc957ba415 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Thu, 30 May 2024 13:50:18 +0100 Subject: [PATCH 54/57] Revert "[REVERTME] enable CI on PRs in to this branch" This reverts commit 6c32c28413067ecf1d057c1b8e4e06a6d3ee1f5a. --- .github/workflows/ci-py.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index 187d69f2e..dfff0d309 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -7,7 +7,6 @@ on: pull_request: branches: - main - - ss2165/pybuild merge_group: types: [checks_requested] workflow_dispatch: {} From 13e54f60c4fca83740761ddca8f20e9cd8afce16 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Wed, 29 May 2024 18:52:18 +0100 Subject: [PATCH 55/57] ci: use binary build stage in python CI (#1135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit passing run: https://github.com/CQCL/hugr/actions/runs/9289115228/job/25562276103 --------- Co-authored-by: Agustín Borgna <121866228+aborgna-q@users.noreply.github.com> --- .github/workflows/ci-py.yml | 69 ++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml index dfff0d309..2d176dfef 100644 --- a/.github/workflows/ci-py.yml +++ b/.github/workflows/ci-py.yml @@ -13,6 +13,8 @@ on: env: SCCACHE_GHA_ENABLED: "true" + HUGR_BIN_DIR: ${{ github.workspace }}/target/debug + HUGR_BIN: ${{ github.workspace }}/target/debug/hugr jobs: # Check if changes were made to the relevant files. @@ -68,8 +70,59 @@ jobs: - name: Lint with ruff run: poetry run ruff check + build_binary: + needs: changes + if: ${{ needs.changes.outputs.python == 'true' }} + + name: Build HUGR binary + runs-on: ubuntu-latest + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + + steps: + - uses: actions/checkout@v4 + - uses: mozilla-actions/sccache-action@v0.0.4 + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + - name: Build HUGR binary + run: cargo build -p hugr-cli + - name: Upload the binary to the artifacts + uses: actions/upload-artifact@v4 + with: + name: hugr_binary + path: target/debug/hugr + test: + needs: [changes, build_binary] + if: ${{ needs.changes.outputs.python == 'true' }} + name: check python ${{ matrix.python-version }} + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ['3.10', '3.12'] + steps: + - uses: actions/checkout@v4 + - name: Install poetry + run: pipx install poetry + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "poetry" + + - name: Install the project libraries + run: poetry install + - name: Download the hugr binary + uses: actions/download-artifact@v4 + with: + name: hugr_binary + path: ${{env.HUGR_BIN_DIR}} + - name: Run tests - run: poetry run pytest + run: | + chmod +x $HUGR_BIN + poetry run pytest # Ensure that the serialization schema is up to date serialization-schema: @@ -106,7 +159,7 @@ jobs: # even if they are skipped due to no changes in the relevant files. required-checks: name: Required checks 🐍 - needs: [changes, check, serialization-schema] + needs: [changes, check, test, serialization-schema] if: ${{ !cancelled() }} runs-on: ubuntu-latest steps: @@ -128,7 +181,7 @@ jobs: echo "All required checks passed" coverage: - needs: [changes, check] + needs: [changes, test] # Run only if there are changes in the relevant files and the check job passed or was skipped if: always() && !failure() && !cancelled() && needs.changes.outputs.python == 'true' && github.event_name != 'merge_group' runs-on: ubuntu-latest @@ -146,9 +199,17 @@ jobs: - name: Install the project libraries run: poetry install + - name: Download the hugr binary + uses: actions/download-artifact@v4 + with: + name: hugr_binary + path: ${{env.HUGR_BIN_DIR}} + - name: Run python tests with coverage instrumentation - run: poetry run pytest --cov=./ --cov-report=xml + run: | + chmod +x $HUGR_BIN + poetry run pytest --cov=./ --cov-report=xml - name: Upload python coverage to codecov.io uses: codecov/codecov-action@v4 From 13fcde3f233f72adb96483c71231ada54af57d67 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Thu, 30 May 2024 13:46:50 +0100 Subject: [PATCH 56/57] feat: put hugr.py behind an underscore --- hugr-py/src/hugr/{hugr.py => _hugr.py} | 0 hugr-py/tests/test_hugr_build.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename hugr-py/src/hugr/{hugr.py => _hugr.py} (100%) diff --git a/hugr-py/src/hugr/hugr.py b/hugr-py/src/hugr/_hugr.py similarity index 100% rename from hugr-py/src/hugr/hugr.py rename to hugr-py/src/hugr/_hugr.py diff --git a/hugr-py/tests/test_hugr_build.py b/hugr-py/tests/test_hugr_build.py index 830e5734e..522da06f6 100644 --- a/hugr-py/tests/test_hugr_build.py +++ b/hugr-py/tests/test_hugr_build.py @@ -3,7 +3,7 @@ import subprocess import os import pathlib -from hugr.hugr import Dfg, Hugr, DummyOp, Node, Command, Wire, Op +from hugr._hugr import Dfg, Hugr, DummyOp, Node, Command, Wire, Op from hugr.serialization import SerialHugr import hugr.serialization.tys as stys import hugr.serialization.ops as sops From dc71629c66da304b7245fa2d24458d72fa014a2c Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Fri, 31 May 2024 17:23:20 +0100 Subject: [PATCH 57/57] add direction to _Port --- hugr-py/src/hugr/_hugr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hugr-py/src/hugr/_hugr.py b/hugr-py/src/hugr/_hugr.py index 1cdffd808..d7552113f 100644 --- a/hugr-py/src/hugr/_hugr.py +++ b/hugr-py/src/hugr/_hugr.py @@ -32,6 +32,7 @@ class Direction(Enum): class _Port: node: Node offset: int + direction: ClassVar[Direction] @dataclass(frozen=True, eq=True, order=True)