Skip to content

Commit ce52cf1

Browse files
authored
✨ Added pickle support to Aig (#145)
* 🚧 `pickle` support for AIGs * 🚧 Initial tests for `pickle` support * ✏️ Fixed a typo * ✨ Add `write_dot` function for AIG network visualization * 🔥 Remove `pybind11` dependency from build requirements * 🔧 Add type stubs for `matplotlib` and `networkx` to pre-commit configuration * 🔧 Ignore security warnings for `pickle` usage in `pyproject.toml` * 🚧 Save commit * 🐛 Fixed `pickle` support via `AigIndexList` * ✅ Test cases demonstrating that `pickle` support works now * 🏷️ Add parameter name and type hints for `pickle` support * ✅ Add tests for exception handling in Aig unpickling * 🎨 Move import statements for `pickle` into the test cases where explicitly needed * ✅ Add test for pickling and unpickling multiple AIGs * 📝 Add documentation for `pickle` support * 🎨 Refactor AIG visualization code and remove unused dependencies * 🎨 Update AIG documentation with improved section headings and additional details on AIG views * 🔒 Update lock file * 🔧 Update pre-commit configuration to skip mypy checks due to memory restrictions * ✅ Add unit test for `write_dot` function to verify file creation and content * 📝 Add documentation on `write_dot`
1 parent 61fad08 commit ce52cf1

20 files changed

+2292
-1930
lines changed

.pre-commit-config.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ ci:
1111
autoupdate_commit_msg: "⬆️ Bump pre-commit hooks"
1212
autoupdate_schedule: quarterly
1313
autofix_commit_msg: "🎨 Incorporated pre-commit fixes"
14+
skip: [mypy]
1415

15-
exclude: "^(libs/|docs/examples/i10\\.aig$)"
16+
exclude: "^(libs/|docs/examples/)"
1617

1718
repos:
1819
# Standard hooks
@@ -71,6 +72,8 @@ repos:
7172
additional_dependencies:
7273
- nox
7374
- pytest
75+
- matplotlib-stubs
76+
- networkx-stubs
7477

7578
# clang-format the C++ part of the code base
7679
- repo: https://github.com/pre-commit/mirrors-clang-format

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,14 @@ Verilog and PLA.
205205
#### ✏️ Writing
206206

207207
```python
208-
from aigverse import write_aiger, write_verilog
208+
from aigverse import write_aiger, write_verilog, write_dot
209209

210210
# Write an AIG network to an AIGER file
211211
write_aiger(aig, "example.aig")
212212
# Write an AIG network to a Verilog file
213213
write_verilog(aig, "example.v")
214+
# Write an AIG network to a DOT file
215+
write_dot(aig, "example.dot")
214216
```
215217

216218
#### 👓 Parsing
@@ -235,6 +237,23 @@ aig4 = read_pla_into_aig("example.pla")
235237
Additionally, you can read AIGER files into sequential AIGs using `read_aiger_into_sequential_aig` and
236238
`read_ascii_aiger_into_sequential_aig`.
237239

240+
### 🥒 `pickle` Support
241+
242+
AIGs support Python's `pickle` protocol, allowing you to serialize and deserialize AIG objects for persistent storage or
243+
interface with data science or machine learning workflows.
244+
245+
```python
246+
import pickle
247+
248+
with open("aig.pkl", "wb") as f:
249+
pickle.dump(aig, f)
250+
251+
with open("aig.pkl", "rb") as f:
252+
unpickled_aig = pickle.load(f)
253+
```
254+
255+
You can also pickle multiple AIGs at once by storing them in a tuple or list.
256+
238257
### 🔌 Adapters
239258

240259
Adapters provide alternative representations of AIGs for integration with other tools or workflows.

bindings/aigverse/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
to_edge_list,
3535
to_index_list,
3636
write_aiger,
37+
write_dot,
3738
write_verilog,
3839
)
3940

@@ -69,5 +70,6 @@
6970
"to_edge_list",
7071
"to_index_list",
7172
"write_aiger",
73+
"write_dot",
7274
"write_verilog",
7375
]

bindings/aigverse/aigverse.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "aigverse/io/read_pla.hpp"
1414
#include "aigverse/io/read_verilog.hpp"
1515
#include "aigverse/io/write_aiger.hpp"
16+
#include "aigverse/io/write_dot.hpp"
1617
#include "aigverse/io/write_verilog.hpp"
1718
#include "aigverse/networks/logic_networks.hpp"
1819
#include "aigverse/truth_tables/operations.hpp"
@@ -55,6 +56,7 @@ PYBIND11_MODULE(pyaigverse, m, pybind11::mod_gil_not_used())
5556
aigverse::read_pla(m);
5657
aigverse::read_verilog(m);
5758
aigverse::write_verilog(m);
59+
aigverse::write_dot(m);
5860

5961
/**
6062
* Adapters

bindings/aigverse/include/aigverse/adapters/index_list.hpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ void ntk_index_list(pybind11::module& m, const std::string& network_name)
3737
/**
3838
* Index list.
3939
*/
40-
using IndexList = mockturtle::xag_index_list<true>;
40+
using IndexList = aigverse::aig_index_list;
4141
py::class_<IndexList>(m, fmt::format("{}IndexList", network_name).c_str())
4242
.def(py::init<const uint32_t>(), "num_pis"_a = 0)
4343
.def(py::init<const std::vector<uint32_t>&>(), "values"_a)
@@ -156,7 +156,7 @@ namespace fmt
156156

157157
// make index_list compatible with fmt::format
158158
template <>
159-
struct formatter<mockturtle::xag_index_list<true>>
159+
struct formatter<aigverse::aig_index_list>
160160
{
161161
template <typename ParseContext>
162162
constexpr auto parse(ParseContext& ctx)
@@ -165,7 +165,7 @@ struct formatter<mockturtle::xag_index_list<true>>
165165
}
166166

167167
template <typename FormatContext>
168-
auto format(const mockturtle::xag_index_list<true>& il, FormatContext& ctx) const
168+
auto format(const aigverse::aig_index_list& il, FormatContext& ctx) const
169169
{
170170
std::vector<std::tuple<uint32_t, uint32_t>> gates{};
171171
gates.reserve(il.num_gates());
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// Created by marcel on 22.04.24.
3+
//
4+
5+
#ifndef AIGVERSE_WRITE_DOT_HPP
6+
#define AIGVERSE_WRITE_DOT_HPP
7+
8+
#include <mockturtle/io/write_dot.hpp>
9+
#include <mockturtle/networks/aig.hpp>
10+
#include <pybind11/pybind11.h>
11+
#include <pybind11/stl.h>
12+
13+
#include <string>
14+
15+
namespace aigverse
16+
{
17+
18+
namespace detail
19+
{
20+
21+
template <typename Ntk>
22+
void write_dot(pybind11::module& m)
23+
{
24+
using namespace pybind11::literals;
25+
26+
m.def(
27+
"write_dot", [](const Ntk& ntk, const std::string& filename) { mockturtle::write_dot(ntk, filename); },
28+
"network"_a, "filename"_a);
29+
}
30+
31+
} // namespace detail
32+
33+
inline void write_dot(pybind11::module& m)
34+
{
35+
detail::write_dot<mockturtle::aig_network>(m);
36+
}
37+
38+
} // namespace aigverse
39+
40+
#endif // AIGVERSE_WRITE_DOT_HPP

bindings/aigverse/include/aigverse/networks/logic_networks.hpp

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,45 @@ void network(pybind11::module& m, const std::string& network_name)
232232
.def(
233233
"is_nary_or", [](const Ntk& ntk, const Node& n) { return ntk.is_nary_or(n); }, "n"_a)
234234

235-
.def("cleanup_dangling", [](Ntk& ntk) { ntk = mockturtle::cleanup_dangling(ntk); });
235+
// pickle support
236+
.def(py::pickle(
237+
[](const Ntk& ntk) // __getstate__
238+
{
239+
aigverse::aig_index_list il{};
240+
mockturtle::encode(il, ntk);
241+
return py::make_tuple(py::cast(il.raw()));
242+
},
243+
[](const py::tuple& state) // __setstate__
244+
{
245+
if (state.size() != 1)
246+
{
247+
throw pybind11::value_error(
248+
"Invalid state: expected a tuple of size 1 containing an index list");
249+
}
250+
try
251+
{
252+
aigverse::aig_index_list il{state[0].cast<std::vector<uint32_t>>()};
253+
254+
Ntk ntk{};
255+
mockturtle::decode(ntk, il);
256+
return ntk;
257+
}
258+
catch (const pybind11::cast_error& e)
259+
{
260+
throw pybind11::value_error(
261+
fmt::format("Invalid state: expected an index list. {}", e.what()));
262+
}
263+
catch (const std::exception& e)
264+
{
265+
throw pybind11::value_error(fmt::format("Failed to restore network state: {}", e.what()));
266+
}
267+
}),
268+
"state"_a)
269+
270+
// clean up dangling nodes (after optimization)
271+
.def("cleanup_dangling", [](Ntk& ntk) { ntk = mockturtle::cleanup_dangling(ntk); })
272+
273+
;
236274

237275
/**
238276
* Depth network.

bindings/aigverse/include/aigverse/types.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <kitty/dynamic_truth_table.hpp>
99
#include <mockturtle/networks/aig.hpp>
1010
#include <mockturtle/networks/sequential.hpp>
11+
#include <mockturtle/utils/index_list.hpp>
1112
#include <mockturtle/views/depth_view.hpp>
1213
#include <mockturtle/views/fanout_view.hpp>
1314

@@ -30,6 +31,10 @@ using fanouts_aig = mockturtle::fanout_view<aig>;
3031
* Alias for the sequential AIG.
3132
*/
3233
using sequential_aig = mockturtle::sequential<aig>;
34+
/**
35+
* Alias for the AIG index list.
36+
*/
37+
using aig_index_list = mockturtle::xag_index_list<true>;
3338
/**
3439
* Alias for the truth table.
3540
*/

bindings/aigverse/pyaigverse.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ class Aig:
7777
def is_xor3(self, n: AigNode | int) -> bool: ...
7878
def is_nary_and(self, n: AigNode | int) -> bool: ...
7979
def is_nary_or(self, n: AigNode | int) -> bool: ...
80+
def __getstate__(self) -> tuple[list[int]]: ...
81+
def __setstate__(self, state: tuple[list[int]]) -> Aig: ...
8082
def cleanup_dangling(self) -> None: ...
8183

8284
class DepthAig(Aig):
@@ -139,10 +141,11 @@ def read_aiger_into_aig(filename: str) -> Aig: ...
139141
def read_ascii_aiger_into_aig(filename: str) -> Aig: ...
140142
def read_aiger_into_sequential_aig(filename: str) -> SequentialAig: ...
141143
def read_ascii_aiger_into_sequential_aig(filename: str) -> SequentialAig: ...
142-
def write_aiger(ntk: Aig, filename: str) -> None: ...
143144
def read_pla_into_aig(filename: str) -> Aig: ...
144145
def read_verilog_into_aig(filename: str) -> Aig: ...
146+
def write_aiger(ntk: Aig, filename: str) -> None: ...
145147
def write_verilog(ntk: Aig, filename: str) -> None: ...
148+
def write_dot(ntk: Aig, filename: str) -> None: ...
146149

147150
class AigEdge:
148151
def __init__(
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import annotations
2+
3+
import tempfile
4+
from pathlib import Path
5+
6+
from aigverse import Aig, write_dot
7+
8+
# Get the temporary directory as a Path object
9+
temp_dir = Path(tempfile.gettempdir())
10+
11+
12+
def test_write_dot_creates_file() -> None:
13+
aig = Aig()
14+
x1 = aig.create_pi()
15+
x2 = aig.create_pi()
16+
f = aig.create_and(x1, x2)
17+
aig.create_po(f)
18+
19+
dot_path = temp_dir / "aig.dot"
20+
if dot_path.exists():
21+
dot_path.unlink()
22+
23+
write_dot(aig, str(dot_path))
24+
assert dot_path.exists()
25+
26+
content = dot_path.read_text()
27+
28+
expected = """
29+
digraph {
30+
rankdir=BT;
31+
32+
0 [label="0",shape=box,style=filled,fillcolor=snow2]
33+
1 [label="1",shape=triangle,style=filled,fillcolor=snow2]
34+
2 [label="2",shape=triangle,style=filled,fillcolor=snow2]
35+
3 [label="3",shape=ellipse,style=filled,fillcolor=white]
36+
37+
po0 [shape=invtriangle,style=filled,fillcolor=snow2]
38+
39+
1 -> 3 [style=solid]
40+
2 -> 3 [style=solid]
41+
3 -> po0 [style=solid]
42+
43+
{rank = same; 0; 1; 2; }
44+
{rank = same; 3; }
45+
{rank = same; po0; }
46+
}
47+
"""
48+
49+
def normalize(s: str) -> str:
50+
return "".join(s.split())
51+
52+
assert normalize(content) == normalize(expected)

0 commit comments

Comments
 (0)