diff --git a/docs/aigs.md b/docs/aigs.md index 7de7aee7..3f6f79e7 100644 --- a/docs/aigs.md +++ b/docs/aigs.md @@ -276,6 +276,60 @@ for node in fanout_aig.fanouts(aig.get_node(n4)): print(f" Node {node}") ``` +### Cut Views + +The {py:class}`~aigverse.networks.Cut` class provides an isolated view on a single cut in a network. A cut has a single output (root) and a set of leaves (inputs). This is useful for analyzing subgraphs or extracting portions of a larger network. + +```{code-cell} ipython3 +from aigverse.networks import Aig, Cut + +# Create a sample AIG +aig = Aig() +x1 = aig.create_pi() +x2 = aig.create_pi() +x3 = aig.create_pi() + +# Create logic +f1 = aig.create_and(x1, x2) +f2 = aig.create_and(f1, x3) +aig.create_po(f2) + +# Create a small cut view with x1, x2 as leaves and f1 as root +# This is valid because f1 only depends on x1 and x2 +cut = Cut(aig, [x1, x2], f1) + +print(f"Small cut has {cut.num_pis} PIs (leaves)") +print(f"Small cut has {cut.num_pos} POs (roots)") +print(f"Small cut has {cut.num_gates} gates") + +# Iterate over leaves (PIs in the cut) +print("\nLeaf nodes in small cut:") +for leaf in cut.pis(): + print(f" Leaf: {leaf}") + +# Iterate over gates in the cut +print("\nGates in small cut:") +for gate in cut.gates(): + print(f" Gate: {gate}") + +# Create a larger cut with all PIs as leaves and f2 as root +# This cut includes both f1 and f2 gates +cut2 = Cut(aig, [x1, x2, x3], f2) + +print(f"\nLarger cut has {cut2.num_pis} PIs (leaves)") +print(f"Larger cut has {cut2.num_pos} POs (roots)") +print(f"Larger cut has {cut2.num_gates} gates") + +# The gates include both f1 and f2 +print("\nGates in larger cut:") +for gate in cut2.gates(): + print(f" Gate: {gate}") +``` + +:::{note} +A cut is only valid if all dependencies of the root node are either included in the leaves or can be reached through nodes within the cut. The cut view clears all nodes' visited flags before construction to ensure the cut is constructed correctly. +::: + ### Sequential AIGs {py:class}`~aigverse.networks.SequentialAig`s extend standard AIGs to include registers, which allow modeling sequential circuits diff --git a/python/aigverse/networks.pyi b/python/aigverse/networks.pyi index 76b82da2..3ac51907 100644 --- a/python/aigverse/networks.pyi +++ b/python/aigverse/networks.pyi @@ -498,6 +498,96 @@ class FanoutAig(Aig): def fanouts(self, n: int) -> list[int]: """Returns fanout nodes of node ``n``.""" +class Cut: + """Implements an isolated view on a single cut in a network. + + This view creates a network from a single cut with a single output `root` + and a set of `leaves`. This is a standalone structural descriptor; it does + not inherit from the network type and only exposes read-only inspection + methods. + + Note: + This view clears all nodes' visited flags before construction to ensure + the cut is constructed correctly. The view guarantees that all nodes in + the view will have a 0 visited flag after construction. + """ + + @overload + def __init__(self, ntk: Aig, leaves: Sequence[int], root: AigSignal) -> None: + """Creates a cut view from a network, leaf nodes, and root signal. + + Args: + ntk: The base network. + leaves: Vector of leaf nodes (boundary of the cut). + root: The root signal (output) of the cut. + """ + + @overload + def __init__(self, ntk: Aig, leaves: Sequence[AigSignal], root: AigSignal) -> None: + """Creates a cut view from a network, leaf signals, and root signal. + + Args: + ntk: The base network. + leaves: Vector of leaf signals (boundary of the cut). + root: The root signal (output) of the cut. + """ + + def clone(self) -> Cut: + """Creates a structural copy of the cut view.""" + + def __copy__(self) -> Cut: + """Returns a shallow copy of the cut view.""" + + def __deepcopy__(self, memo: dict) -> Cut: + """Returns a deep copy of the cut view.""" + + def nodes(self) -> list[int]: + """Returns a list of all nodes in the cut view.""" + + def gates(self) -> list[int]: + """Returns a list of all gate nodes in the cut view.""" + + def pis(self) -> list[int]: + """Returns a list of all primary input (leaf) nodes in the cut view.""" + + def pos(self) -> list[AigSignal]: + """Returns a list containing the root signal of the cut view.""" + + def is_pi(self, n: int) -> bool: + """Returns whether ``n`` is a primary input (leaf) in the cut view.""" + + @property + def size(self) -> int: + """Number of nodes in the cut view.""" + + @property + def num_pis(self) -> int: + """Number of primary inputs (leaves) in the cut view.""" + + @property + def num_pos(self) -> int: + """Number of primary outputs (always 1 for cut view).""" + + @property + def num_gates(self) -> int: + """Number of logic gates in the cut view.""" + + def node_to_index(self, n: int) -> int: + """Returns the integer index of a node.""" + + def index_to_node(self, index: int) -> int: + """Returns the node for an index.""" + + def to_index_list(self) -> AigIndexList: + """Converts the cut view to an index-list encoding. + + Only the cut's restricted node set is encoded. The resulting index list + can be decoded into a standalone Aig via ``AigIndexList.to_aig()``. + + Returns: + The corresponding index-list representation. + """ + class AigRegister: """Represents metadata for one sequential register.""" diff --git a/src/aigverse/networks/logic_networks.cpp b/src/aigverse/networks/logic_networks.cpp index 1b84ed94..5d1fa3a1 100644 --- a/src/aigverse/networks/logic_networks.cpp +++ b/src/aigverse/networks/logic_networks.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,7 @@ #include #include +#include #include #include #include @@ -529,6 +531,131 @@ Preserves only combinational structure and does not capture augmented view metad }, nb::arg("n"), R"pb(Returns fanout nodes of node ``n``.)pb"); + using CutNtk = mockturtle::cut_view; + + auto clear_visited = [](const Ntk& ntk) { ntk.foreach_node([&ntk](const auto& n) { ntk.set_visited(n, 0); }); }; + + nb::class_(m, "Cut", + R"pb(Implements an isolated view on a single cut in a network. + +This view creates a network from a single cut with a single output `root` +and a set of `leaves`. This is a standalone structural descriptor; it does +not inherit from the network type and only exposes read-only inspection +methods. + +Note: + This view clears all nodes' visited flags before construction to ensure + the cut is constructed correctly. The view guarantees that all nodes in + the view will have a 0 visited flag after construction.)pb") + .def( + "__init__", + [clear_visited](CutNtk* self, const Ntk& ntk, const std::vector& leaves, const Signal& root) + { + clear_visited(ntk); + new (self) CutNtk(ntk, leaves, root); + }, + nb::arg("ntk"), nb::arg("leaves"), nb::arg("root"), + R"pb(Creates a cut view from a network, leaf nodes, and root signal. + +Args: + ntk: The base network. + leaves: Vector of leaf nodes (boundary of the cut). + root: The root signal (output) of the cut.)pb") + .def( + "__init__", + [clear_visited](CutNtk* self, const Ntk& ntk, const std::vector& leaves, const Signal& root) + { + clear_visited(ntk); + new (self) CutNtk(ntk, leaves, root); + }, + nb::arg("ntk"), nb::arg("leaves"), nb::arg("root"), + R"pb(Creates a cut view from a network, leaf signals, and root signal. + +Args: + ntk: The base network. + leaves: Vector of leaf signals (boundary of the cut). + root: The root signal (output) of the cut.)pb") + .def( + "clone", [](const CutNtk& ntk) { return CutNtk{ntk}; }, R"pb(Creates a structural copy of the cut view.)pb") + .def( + "__copy__", [](const CutNtk& ntk) { return CutNtk{ntk}; }, R"pb(Returns a shallow copy of the cut view.)pb") + .def( + "__deepcopy__", [](const CutNtk& ntk, const nb::dict&) { return CutNtk{ntk}; }, nb::arg("memo"), + R"pb(Returns a deep copy of the cut view.)pb") + .def( + "nodes", [](const CutNtk& ntk) { return collect_nodes(ntk); }, + R"pb(Returns a list of all nodes in the cut view.)pb") + .def( + "gates", + [](const CutNtk& ntk) + { + std::vector gates{}; + gates.reserve(static_cast(ntk.num_gates())); + ntk.foreach_gate([&gates](const auto& g) { gates.push_back(g); }); + return gates; + }, + R"pb(Returns a list of all gate nodes in the cut view.)pb") + .def( + "pis", + [](const CutNtk& ntk) + { + std::vector pis{}; + pis.reserve(static_cast(ntk.num_pis())); + ntk.foreach_pi([&pis](const auto& pi) { pis.push_back(pi); }); + return pis; + }, + R"pb(Returns a list of all primary input (leaf) nodes in the cut view.)pb") + .def( + "pos", + [](const CutNtk& ntk) + { + std::vector pos{}; + pos.reserve(static_cast(ntk.num_pos())); + ntk.foreach_po([&pos](const auto& po) { pos.push_back(po); }); + return pos; + }, + R"pb(Returns a list containing the root signal of the cut view.)pb") + .def( + "is_pi", [](const CutNtk& ntk, const Node& n) { return ntk.is_pi(n); }, nb::arg("n"), + R"pb(Returns whether ``n`` is a primary input (leaf) in the cut view.)pb") + .def_prop_ro( + "size", [](const CutNtk& ntk) { return ntk.size(); }, R"pb(Number of nodes in the cut view.)pb") + .def_prop_ro( + "num_pis", [](const CutNtk& ntk) { return ntk.num_pis(); }, + R"pb(Number of primary inputs (leaves) in the cut view.)pb") + .def_prop_ro( + "num_pos", [](const CutNtk& ntk) { return ntk.num_pos(); }, + R"pb(Number of primary outputs (always 1 for cut view).)pb") + .def_prop_ro( + "num_gates", [](const CutNtk& ntk) { return ntk.num_gates(); }, + R"pb(Number of logic gates in the cut view.)pb") + .def( + "node_to_index", [](const CutNtk& ntk, const Node& n) { return ntk.node_to_index(n); }, nb::arg("n"), + R"pb(Returns the integer index of a node.)pb") + .def( + "index_to_node", [](const CutNtk& ntk, const uint32_t index) { return ntk.index_to_node(index); }, + nb::arg("index"), R"pb(Returns the node for an index.)pb") + .def( + "to_index_list", + [](const CutNtk& ntk) + { + aigverse::aig_index_list il{}; + mockturtle::encode(il, ntk); + return il; + }, + R"pb(Converts the cut view to an index-list encoding. + +Only the cut's restricted node set is encoded. The resulting index list +can be decoded into a standalone Aig via ``AigIndexList.to_aig()``. + +Returns: + The corresponding index-list representation.)pb", + nb::rv_policy::move) + .def( + "__repr__", [](const CutNtk& ntk) + { return fmt::format("Cut(leaves={}, gates={}, size={})", ntk.num_pis(), ntk.num_gates(), ntk.size()); }, + R"pb(Returns a developer-friendly string representation.)pb"); + using Register = mockturtle::register_t; // NOLINT(readability-identifier-naming) nb::class_(m, fmt::format("{}Register", network_name).c_str(), R"pb(Represents metadata for one sequential register.)pb") diff --git a/test/networks/test_cut.py b/test/networks/test_cut.py new file mode 100644 index 00000000..f5ec97c1 --- /dev/null +++ b/test/networks/test_cut.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import copy + +from aigverse.networks import Aig, Cut + + +def test_cut() -> None: + """Test Cut creation and basic properties.""" + aig = Aig() + x1 = aig.create_pi() + x2 = aig.create_pi() + + f1 = aig.create_and(x1, x2) + aig.create_po(f1) + + # Create a cut view + leaves = [x1, x2] + root = f1 + cut = Cut(aig, leaves, root) + + # Check basic properties + assert hasattr(cut, "size") + assert hasattr(cut, "num_gates") + assert hasattr(cut, "num_pis") + assert hasattr(cut, "num_pos") + + # The cut should have 2 leaves (PIs) and 1 gate + assert cut.num_pis == 2 + assert cut.num_pos == 1 + assert cut.num_gates == 1 + + +def test_cut_with_signals() -> None: + """Test Cut creation using signals.""" + aig = Aig() + a = aig.create_pi() + b = aig.create_pi() + c = aig.create_pi() + d = aig.create_pi() + + f1 = aig.create_and(a, b) + f2 = aig.create_and(c, d) + f3 = aig.create_xor(f1, f2) + aig.create_po(f3) + + # Create a cut view with all PIs as leaves + leaves = [a, b, c, d] + root = f3 + cut = Cut(aig, leaves, root) + + # Should contain all 4 PIs as leaves + assert cut.num_pis == 4 + assert cut.num_gates == 5 + + +def test_cut_iteration() -> None: + """Test iteration over nodes in Cut.""" + aig = Aig() + x1 = aig.create_pi() + x2 = aig.create_pi() + + f1 = aig.create_and(x1, x2) + aig.create_po(f1) + + # Create cut for the output + leaves = [x1, x2] + root = f1 + cut = Cut(aig, leaves, root) + + # Iterate over all nodes + nodes = cut.nodes() + assert len(nodes) == cut.size + assert 0 in nodes # constant node + + # Iterate over PIs + pis = cut.pis() + assert len(pis) == cut.num_pis + + # Iterate over gates + gates = cut.gates() + assert len(gates) == cut.num_gates + + # Iterate over POs + pos = cut.pos() + assert len(pos) == 1 + + +def test_cut_index_mapping() -> None: + """Test node to index and index to node mapping in Cut.""" + aig = Aig() + x1 = aig.create_pi() + x2 = aig.create_pi() + + f1 = aig.create_and(x1, x2) + aig.create_po(f1) + + cut = Cut(aig, [x1, x2], f1) + + # Test node_to_index and index_to_node + for node in cut.nodes(): + index = cut.node_to_index(node) + recovered_node = cut.index_to_node(index) + assert recovered_node == node + + +def test_cut_is_pi() -> None: + """Test is_pi method in Cut.""" + aig = Aig() + x1 = aig.create_pi() + x2 = aig.create_pi() + f1 = aig.create_and(x1, x2) + aig.create_po(f1) + + cut = Cut(aig, [x1, x2], f1) + + # Check that leaf nodes are PIs in the cut + assert cut.is_pi(aig.get_node(x1)) + assert cut.is_pi(aig.get_node(x2)) + + # Check that gate node is not a PI + assert not cut.is_pi(aig.get_node(f1)) + + +def test_cut_repr() -> None: + """Test __repr__ method in Cut.""" + aig = Aig() + x1 = aig.create_pi() + x2 = aig.create_pi() + f1 = aig.create_and(x1, x2) + aig.create_po(f1) + + cut = Cut(aig, [x1, x2], f1) + repr_str = repr(cut) + assert "Cut" in repr_str + assert "leaves=2" in repr_str + assert "gates=1" in repr_str + + +def test_cut_clone_and_copy() -> None: + """Test clone, __copy__, and __deepcopy__ in Cut.""" + aig = Aig() + x1 = aig.create_pi() + x2 = aig.create_pi() + f1 = aig.create_and(x1, x2) + aig.create_po(f1) + + cut = Cut(aig, [x1, x2], f1) + + cloned = cut.clone() + shallow = copy.copy(cut) + deep = copy.deepcopy(cut) + for candidate in (cloned, shallow, deep): + assert isinstance(candidate, Cut) + assert candidate.num_pis == 2 + assert candidate.num_pos == 1 + assert candidate.num_gates == 1 + + +def test_cut_with_nodes() -> None: + """Test Cut creation using nodes instead of signals.""" + aig = Aig() + x1 = aig.create_pi() + x2 = aig.create_pi() + f1 = aig.create_and(x1, x2) + aig.create_po(f1) + + leaves = [aig.get_node(x1), aig.get_node(x2)] + root = f1 + cut = Cut(aig, leaves, root) + assert cut.num_pis == 2 + assert cut.num_pos == 1 + assert cut.num_gates == 1 + assert cut.size == 4 + + +def test_cut_to_index_list() -> None: + """Test that to_index_list() encodes only the cut, not the whole network.""" + aig = Aig() + x1 = aig.create_pi() + x2 = aig.create_pi() + x3 = aig.create_pi() + x4 = aig.create_pi() + + f1 = aig.create_and(x1, x2) + f2 = aig.create_and(x3, x4) + f3 = aig.create_and(f1, f2) + aig.create_po(f3) + + # Create a cut covering only f1 (1 gate, 2 leaves) + cut = Cut(aig, [x1, x2], f1) + assert cut.num_gates == 1 + assert cut.num_pis == 2 + + # to_index_list() should encode only the cut's nodes + il = cut.to_index_list() + assert il.num_gates == 1 + assert il.num_pis == 2 + assert il.num_pos == 1 + + # The full network has 3 gates and 4 PIs — verify we didn't get those + assert il.num_gates != aig.num_gates + assert il.num_pis != aig.num_pis + + # Decode back to a standalone Aig + standalone_aig = il.to_aig() + assert standalone_aig.num_gates == 1 + assert standalone_aig.num_pis == 2 + assert standalone_aig.num_pos == 1