relucent 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: relucent
3
+ Version: 0.1.0
4
+ Author: Blake B. Gaines
5
+ License-Expression: AGPL-3.0-or-later
6
+ Requires-Dist: torch
7
+ Requires-Dist: torchvision
8
+ Requires-Dist: pandas
9
+ Requires-Dist: gurobipy
10
+ Requires-Dist: networkx
11
+ Requires-Dist: numpy
12
+ Requires-Dist: scipy
13
+ Requires-Dist: tqdm
14
+ Requires-Dist: matplotlib
15
+ Requires-Dist: plotly
16
+ Requires-Dist: pillow
17
+ Requires-Dist: pyvis ; extra == 'cli'
18
+ Requires-Dist: kaleido ; extra == 'cli'
19
+ Requires-Python: >=3.13, <3.14
20
+ Project-URL: Homepage, https://github.com/bl-ake/relucent
21
+ Provides-Extra: cli
22
+ Description-Content-Type: text/markdown
23
+
24
+ [![Usable](https://github.com/bl-ake/relucent/actions/workflows/python-package.yml/badge.svg)](https://github.com/bl-ake/relucent/actions/workflows/python-package.yml)
25
+
26
+ # Relucent
27
+ Explore polyhedral complexes associated with ReLU networks
28
+
29
+ ## Environment Setup
30
+ 1. Install Python 3.13
31
+ 2. Install [PyTorch 2.3.0](https://pytorch.org/get-started/previous-versions/#:~:text=org/whl/cpu-,v2.3.0)
32
+ 3. Install the remaining dependencies with `pip install -r requirements.txt`
33
+
34
+ ## Code Structure
35
+ * [model.py](src/relucent/model.py): PyTorch Module that acts as an interface between the model and the rest of the code
36
+ * [poly.py](src/relucent/poly.py): Class for calculations involving individual polyhedrons (e.g. computing boundaries, neighbors, volume)
37
+ * [complex.py](src/relucent/complex.py): Class for calculations involving the polyhedral cplx (e.g. polyhedron search, connectivity graph calculation)
38
+ * [convert_model.py](src/relucent/convert_model.py): Utilities for converting various PyTorch.nn layers to Linear layers
39
+ * [bvs.py](src/relucent/bvs.py): Data structures for storing large numbers of sign vectors
40
+
41
+ ## Obtaining a Gurobi License
42
+
43
+ **The following steps are not necessary when replicating the experiments from the paper.**
44
+
45
+ Without a [license](https://support.gurobi.com/hc/en-us/articles/12872879801105-How-do-I-retrieve-and-set-up-a-Gurobi-license), Gurobi will only work with a limited feature set. This includes a limit on the number of decision variables in the models it can solve, which limits the size of the networks this code is able to analyze. There are multiple ways to install the software, but we recommend the following steps to those eligible for an academic license:
46
+ 1. Install the [Gurobi Python library](https://pypi.org/project/gurobipy/), for example using `pip install gurobipy`
47
+ 2. [Obtain a Gurobi license](https://support.gurobi.com/hc/en-us/articles/360040541251-How-do-I-obtain-a-free-academic-license) (Note: a WLS license will limit the number of concurrent sessions across multiple devices, which can result in slowdowns when using this library on different machines simultaneously.)
48
+ 3. In your Conda environment, run `grbgetkey` followed by your license key
@@ -0,0 +1,25 @@
1
+ [![Usable](https://github.com/bl-ake/relucent/actions/workflows/python-package.yml/badge.svg)](https://github.com/bl-ake/relucent/actions/workflows/python-package.yml)
2
+
3
+ # Relucent
4
+ Explore polyhedral complexes associated with ReLU networks
5
+
6
+ ## Environment Setup
7
+ 1. Install Python 3.13
8
+ 2. Install [PyTorch 2.3.0](https://pytorch.org/get-started/previous-versions/#:~:text=org/whl/cpu-,v2.3.0)
9
+ 3. Install the remaining dependencies with `pip install -r requirements.txt`
10
+
11
+ ## Code Structure
12
+ * [model.py](src/relucent/model.py): PyTorch Module that acts as an interface between the model and the rest of the code
13
+ * [poly.py](src/relucent/poly.py): Class for calculations involving individual polyhedrons (e.g. computing boundaries, neighbors, volume)
14
+ * [complex.py](src/relucent/complex.py): Class for calculations involving the polyhedral cplx (e.g. polyhedron search, connectivity graph calculation)
15
+ * [convert_model.py](src/relucent/convert_model.py): Utilities for converting various PyTorch.nn layers to Linear layers
16
+ * [bvs.py](src/relucent/bvs.py): Data structures for storing large numbers of sign vectors
17
+
18
+ ## Obtaining a Gurobi License
19
+
20
+ **The following steps are not necessary when replicating the experiments from the paper.**
21
+
22
+ Without a [license](https://support.gurobi.com/hc/en-us/articles/12872879801105-How-do-I-retrieve-and-set-up-a-Gurobi-license), Gurobi will only work with a limited feature set. This includes a limit on the number of decision variables in the models it can solve, which limits the size of the networks this code is able to analyze. There are multiple ways to install the software, but we recommend the following steps to those eligible for an academic license:
23
+ 1. Install the [Gurobi Python library](https://pypi.org/project/gurobipy/), for example using `pip install gurobipy`
24
+ 2. [Obtain a Gurobi license](https://support.gurobi.com/hc/en-us/articles/360040541251-How-do-I-obtain-a-free-academic-license) (Note: a WLS license will limit the number of concurrent sessions across multiple devices, which can result in slowdowns when using this library on different machines simultaneously.)
25
+ 3. In your Conda environment, run `grbgetkey` followed by your license key
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["uv_build >= 0.9.5, <0.10.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "relucent"
7
+ version = "0.1.0"
8
+ requires-python = ">= 3.13, <3.14"
9
+ dependencies = [ ## TODO: Constrain Versions
10
+ "torch",
11
+ "torchvision",
12
+ "pandas",
13
+ "gurobipy",
14
+ "networkx",
15
+ "numpy",
16
+ "scipy",
17
+ "tqdm",
18
+ "matplotlib",
19
+ "plotly",
20
+ "Pillow",
21
+ ]
22
+ authors = [{ name = "Blake B. Gaines" }]
23
+ license = "AGPL-3.0-or-later"
24
+ readme = "README.md"
25
+
26
+ [project.optional-dependencies]
27
+ cli = ["pyvis", "kaleido"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/bl-ake/relucent"
@@ -0,0 +1,7 @@
1
+ from .bvs import BVManager
2
+ from .complex import Complex
3
+ from .model import NN, get_mlp_model
4
+ from .convert_model import convert
5
+ from .utils import get_env, split_sequential, data_graph, set_seeds
6
+
7
+ __all__ = [Complex, NN, get_mlp_model, BVManager, convert, get_env, split_sequential, data_graph, set_seeds]
@@ -0,0 +1,221 @@
1
+ from heapq import heappop, heappush
2
+
3
+ from torch import Tensor
4
+
5
+ from .poly import encode_bv
6
+
7
+
8
+ class BVManager:
9
+ def __init__(self):
10
+ self.index2bv = list()
11
+ self.tag2index = dict() ## Tags are just hashable versions of bvs, should be unique
12
+ self._len = 0
13
+
14
+ def _get_tag(self, bv):
15
+ if isinstance(bv, Tensor):
16
+ bv = bv.detach().cpu().numpy()
17
+ return encode_bv(bv)
18
+
19
+ def add(self, bv):
20
+ tag = self._get_tag(bv)
21
+ if tag not in self.tag2index:
22
+ self.tag2index[tag] = len(self.index2bv)
23
+ self.index2bv.append(bv)
24
+ self._len += 1
25
+
26
+ def __getitem__(self, bv):
27
+ tag = self._get_tag(bv)
28
+ index = self.tag2index[tag]
29
+ if self.index2bv[index] is None:
30
+ raise KeyError
31
+ return index
32
+
33
+ def __contains__(self, bv):
34
+ tag = self._get_tag(bv)
35
+ if tag not in self.tag2index:
36
+ return False
37
+ return self.index2bv[self.tag2index[tag]] is not None
38
+
39
+ def __delitem__(self, bv):
40
+ tag = self._get_tag(bv)
41
+ index = self.tag2index[tag]
42
+ self.index2bv[index] = None
43
+ self._len -= 1
44
+
45
+ def __iter__(self):
46
+ return iter((bv for bv in self.index2bv if bv is not None))
47
+
48
+ def __len__(self):
49
+ return self._len
50
+
51
+
52
+ # TODO: Move to utils as general priority queue
53
+ class BVPriorityQueue:
54
+ ## From https://docs.python.org/3/library/heapq.html
55
+ REMOVED = "<removed-task>" # placeholder for a removed task
56
+
57
+ def __init__(self):
58
+ self.pq = [] # list of entries arranged in a heap
59
+ self.entry_finder = {} # mapping of tasks to entries
60
+ self.counter = 0 # unique sequence count
61
+
62
+ def push(self, task, priority=0):
63
+ "Add a new task or update the priority of an existing task"
64
+ bv, *task = task
65
+ task = tuple(task)
66
+ if task in self.entry_finder:
67
+ self.remove_task(task)
68
+ entry = [priority, self.counter, bv, task]
69
+ self.entry_finder[task] = entry
70
+ heappush(self.pq, entry)
71
+ self.counter += 1
72
+
73
+ def remove_task(self, task):
74
+ "Mark an existing task as REMOVED. Raise KeyError if not found."
75
+ entry = self.entry_finder.pop(task)
76
+ entry[-1] = self.REMOVED
77
+
78
+ def pop(self):
79
+ "Remove and return the lowest priority task. Raise KeyError if empty."
80
+ while self.pq:
81
+ _, _, bv, task = heappop(self.pq)
82
+ if task is not self.REMOVED:
83
+ del self.entry_finder[task]
84
+ return bv, *task
85
+ raise KeyError("pop from an empty priority queue")
86
+
87
+ def __len__(self):
88
+ return len(self.entry_finder)
89
+
90
+
91
+ # class BVPriorityQueue:
92
+ # def __init__(self):
93
+ # self.pq = [] # list of entries arranged in a heap
94
+
95
+ # def push(self, task, priority=0):
96
+ # "Add a new task or update the priority of an existing task"
97
+ # # bv, *task = task
98
+ # self.pq.append((priority, task))
99
+ # self.pq.sort(reverse=True, key=lambda x: x[0])
100
+
101
+ # def remove_task(self, task):
102
+ # "Mark an existing task as REMOVED. Raise KeyError if not found."
103
+ # self.pq = [(p, t) for p, t in self.pq if t != task]
104
+
105
+ # def pop(self):
106
+ # "Remove and return the lowest priority task. Raise KeyError if empty."
107
+ # return self.pq.pop(-1)[1]
108
+
109
+ # def __len__(self):
110
+ # return len(self.pq)
111
+
112
+
113
+ # class BVNode:
114
+ # def __init__(self, key):
115
+ # self.key = key ## Key of bv being set
116
+ # self.left = None ## for all nodes in subtree, bv[0, key] = -1
117
+ # self.middle = None ## ...bv[0, key] = 0
118
+ # self.right = None ## ...bv[0, key] = 1
119
+
120
+ # def get_child(self, bv):
121
+ # # return either BVNode or int
122
+ # if bv[0, self.key] == -1:
123
+ # next_node = self.left
124
+ # elif bv[0, self.key] == 0:
125
+ # next_node = self.middle
126
+ # elif bv[0, self.key] == 1:
127
+ # next_node = self.right
128
+ # return next_node
129
+
130
+ # def set_child(self, bv, node):
131
+ # if bv[0, self.key] == -1:
132
+ # self.left = node
133
+ # elif bv[0, self.key] == 0:
134
+ # self.middle = node
135
+ # elif bv[0, self.key] == 1:
136
+ # self.right = node
137
+
138
+ # def print(self, level=0):
139
+ # print(" " * level + str(self.key) + ":")
140
+ # for name, k in zip(("L", "M", "R"), (self.left, self.middle, self.right)):
141
+ # if isinstance(k, BVNode):
142
+ # print(" " * (level + 2) + name)
143
+ # k.print(level=level + 4)
144
+ # elif isinstance(k, int):
145
+ # print(" " * (level + 2) + name + ": leaf " + str(k))
146
+
147
+ # # Trie
148
+ # # Each edge in the tree sets a dimension to a value
149
+ # # Leaf nodes are just indices of bvs in index2bv
150
+ # # TODO: Replace hashmaps in Complex class with this
151
+ # class BVManager:
152
+ # def __init__(self):
153
+ # self.root = BVNode(0)
154
+ # self.index2bv = list()
155
+
156
+ # def add(self, bv):
157
+ # assert bv.ndim == 2
158
+ # node = self.root
159
+ # child = self.root.get_child(bv)
160
+ # while isinstance(child, BVNode):
161
+ # node = child
162
+ # child = node.get_child(bv)
163
+ # if child is None:
164
+ # node.set_child(bv, len(self.index2bv))
165
+ # self.index2bv.append(bv)
166
+ # elif isinstance(child, int): ## TODO: This check should be redundant
167
+ # child_bv = self.index2bv[child]
168
+ # if not isinstance(child_bv, type(bv)):
169
+ # if isinstance(bv, Tensor):
170
+ # bv = bv.detach().cpu().numpy()
171
+ # else:
172
+ # child_bv = child_bv.detach().cpu().numpy()
173
+ # if not (child_bv == bv).all():
174
+ # if child_bv[0, node.key] == bv[0, node.key]: ## TODO: This check should be redundant
175
+ # for i in range(bv.shape[1]):
176
+ # if child_bv[0, i] != bv[0, i]:
177
+ # break
178
+
179
+ # ## Replace the existing child of the node with a new node
180
+ # new_bvnode = BVNode(i)
181
+ # node.set_child(bv, new_bvnode)
182
+
183
+ # ## Set the new node's children
184
+ # new_bvnode.set_child(child_bv, child)
185
+ # new_bvnode.set_child(bv, len(self.index2bv))
186
+ # self.index2bv.append(bv)
187
+ # else:
188
+ # raise ValueError("Something went wrong")
189
+
190
+ # def __getitem__(self, bv):
191
+ # assert bv.ndim == 2
192
+ # node = self.root
193
+ # while isinstance(node, BVNode):
194
+ # node = node.get_child(bv)
195
+ # if isinstance(node, int):
196
+ # found_bv = self.index2bv[node]
197
+ # if not isinstance(bv, type(found_bv)):
198
+ # if isinstance(bv, Tensor):
199
+ # bv = bv.detach().cpu().numpy()
200
+ # else:
201
+ # found_bv = found_bv.detach().cpu().numpy()
202
+ # if (found_bv == bv).all():
203
+ # return node
204
+ # raise KeyError
205
+
206
+ # def __contains__(self, bv):
207
+ # try:
208
+ # self[bv]
209
+ # return True
210
+ # except KeyError:
211
+ # return False
212
+
213
+ # def __iter__(self):
214
+ # return iter(self.index2bv)
215
+
216
+ # def __len__(self):
217
+ # return len(self.index2bv)
218
+
219
+ # def print(self):
220
+ # print("Number of BVs:", len(self))
221
+ # self.root.print()