simplex-tree-classifier 0.1.0__py3-none-any.whl

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,319 @@
1
+ """Hierarchical tree of N-dimensional simplices.
2
+
3
+ A ``SimplexTree`` is a ``Simplex`` that can subdivide itself. Two subdivision
4
+ schemes are supported through this class:
5
+
6
+ * barycentric - recursively split every simplex at its barycenter
7
+ (``_add_barycentric_centers_recursively``), producing a uniform tree;
8
+ * data-driven - split only chosen leaves at their barycenter
9
+ (``subdivide_leaf``), driven externally by classification error.
10
+
11
+ The tree also answers geometric queries used by the classifier:
12
+ ``find_containing_simplex`` and ``find_adjacent_simplexes``.
13
+ """
14
+
15
+ import numpy as np
16
+ from typing import List, Optional, Iterator, Tuple
17
+ from collections import deque
18
+
19
+ from .simplex import Simplex
20
+ from .vertex_registry import VertexRegistry
21
+
22
+
23
+ def make_enclosing_simplex(dimension: int, scale: Optional[float] = None,
24
+ margin: float = 0.05) -> List[Tuple[float, ...]]:
25
+ """Build vertices of a d-simplex that encloses the unit hypercube ``[0, 1]^d``.
26
+
27
+ The simplex has one vertex at the origin and one vertex ``scale * e_i`` along
28
+ each axis, i.e. the region ``{x_i >= 0, sum_i x_i <= scale}``. Choosing
29
+ ``scale > d`` guarantees the whole closed unit cube (whose farthest corner has
30
+ coordinate sum ``d``) lies strictly inside.
31
+
32
+ For ``d == 2`` and ``margin == 0`` this reproduces the classic base triangle
33
+ ``[(0, 0), (2, 0), (0, 2)]``.
34
+
35
+ Args:
36
+ dimension: Number of features ``d``.
37
+ scale: Axis intercept of the enclosing simplex. Defaults to
38
+ ``d * (1 + margin)``.
39
+ margin: Relative slack beyond the unit cube when ``scale`` is not given.
40
+
41
+ Returns:
42
+ List of ``d + 1`` coordinate tuples.
43
+ """
44
+ if dimension < 1:
45
+ raise ValueError("dimension must be >= 1")
46
+ if scale is None:
47
+ scale = dimension * (1.0 + margin)
48
+ origin = tuple(0.0 for _ in range(dimension))
49
+ vertices = [origin]
50
+ for axis in range(dimension):
51
+ vertex = [0.0] * dimension
52
+ vertex[axis] = float(scale)
53
+ vertices.append(tuple(vertex))
54
+ return vertices
55
+
56
+
57
+ class SimplexTree(Simplex):
58
+ def __init__(self, vertices: List[Tuple[float, ...]], tolerance: float = 1e-10,
59
+ registry: Optional[VertexRegistry] = None, _is_child: bool = False):
60
+ if registry is None:
61
+ reg = VertexRegistry(tolerance)
62
+ self._is_root = True
63
+ self._split_counter = [0]
64
+ else:
65
+ reg = registry
66
+ self._is_root = not _is_child
67
+ self._split_counter = None
68
+
69
+ vertex_indices = reg.register_vertices(vertices)
70
+ super().__init__(vertex_indices, reg, tolerance)
71
+
72
+ self.children: List[Optional['SimplexTree']] = []
73
+ self.parent: Optional['SimplexTree'] = None
74
+ self.depth: int = 0
75
+ self._node_count = 1
76
+ self.splitting_point_index: Optional[int] = None
77
+ self.splitting_point_vertex_index: Optional[int] = None
78
+
79
+ def _get_root(self) -> 'SimplexTree':
80
+ node = self
81
+ while node.parent is not None:
82
+ node = node.parent
83
+ return node
84
+
85
+ def _get_next_split_index(self) -> int:
86
+ root = self._get_root()
87
+ idx = root._split_counter[0]
88
+ root._split_counter[0] += 1
89
+ return idx
90
+
91
+ def _add_child(self, child_vertices: List[Tuple[float, ...]]) -> 'SimplexTree':
92
+ child = SimplexTree(child_vertices, self.tolerance, self.registry, _is_child=True)
93
+ child.parent = self
94
+ child.depth = self.depth + 1
95
+ self.children.append(child)
96
+ return child
97
+
98
+ def _is_leaf(self) -> bool:
99
+ return len(self.children) == 0
100
+
101
+ def _get_children(self) -> List['SimplexTree']:
102
+ return self.children.copy()
103
+
104
+ def _remove_splitting_point(self, splitting_point_index: int) -> bool:
105
+ """Remove a splitting point by clearing its children and resetting to a leaf."""
106
+ for node in self._traverse_breadth_first():
107
+ if node.splitting_point_index == splitting_point_index:
108
+ for child in node.children:
109
+ if len(child.children) > 0:
110
+ return False
111
+ node.children.clear()
112
+ node.splitting_point_index = None
113
+ node.splitting_point_vertex_index = None
114
+ return True
115
+ return False
116
+
117
+ def remove_by_leaf_key(self, vertex_key: frozenset) -> bool:
118
+ """Removes a leaf simplex by undoing its parent's split.
119
+
120
+ Clears the parent's splitting point and removes all sibling children,
121
+ restoring the parent to a leaf state.
122
+
123
+ Args:
124
+ vertex_key: Frozenset of vertex indices identifying the leaf to remove
125
+
126
+ Returns:
127
+ True if removal succeeded, False if leaf not found or has children
128
+ """
129
+ for node in self._traverse_breadth_first():
130
+ if frozenset(node.vertex_indices) == vertex_key:
131
+ parent = node.parent
132
+ if parent is not None and parent.splitting_point_index is not None:
133
+ return self._remove_splitting_point(parent.splitting_point_index)
134
+ return False
135
+ return False
136
+
137
+ def _traverse_breadth_first(self) -> Iterator['SimplexTree']:
138
+ queue = deque([self])
139
+ while queue:
140
+ node = queue.popleft()
141
+ yield node
142
+ for child in node._get_children():
143
+ queue.append(child)
144
+
145
+ def get_leaves(self) -> List['SimplexTree']:
146
+ """Returns all leaf nodes (simplices with no children) in the tree."""
147
+ leaves = []
148
+ for node in self._traverse_breadth_first():
149
+ if node._is_leaf():
150
+ leaves.append(node)
151
+ return leaves
152
+
153
+ def find_adjacent_simplexes(self, leaf: 'SimplexTree') -> List['SimplexTree']:
154
+ """Finds all leaf simplexes adjacent to the given leaf.
155
+
156
+ Two leaf simplexes are adjacent if they share at least ``d`` vertices
157
+ (a ``(d-1)``-dimensional face), where ``d`` is the dimension of the space.
158
+
159
+ Walks up the tree level by level - at each ancestor, checks the sibling
160
+ subtrees for adjacent leaves (siblings, cousins, second cousins, etc.).
161
+
162
+ Args:
163
+ leaf: A leaf SimplexTree node
164
+
165
+ Returns:
166
+ List of adjacent leaf SimplexTree nodes
167
+ """
168
+ leaf_vertex_set = set(leaf.vertex_indices)
169
+ min_shared = leaf.dimension
170
+ adjacent = []
171
+ seen = set()
172
+
173
+ current = leaf
174
+ while current.parent is not None:
175
+ parent = current.parent
176
+ for sibling in parent.children:
177
+ if sibling is current:
178
+ continue
179
+ candidates = [sibling] if sibling._is_leaf() else sibling.get_leaves()
180
+ for candidate in candidates:
181
+ if id(candidate) not in seen:
182
+ if len(leaf_vertex_set.intersection(candidate.vertex_indices)) >= min_shared:
183
+ adjacent.append(candidate)
184
+ seen.add(id(candidate))
185
+ if len(adjacent) == leaf.dimension + 1:
186
+ return adjacent
187
+ current = parent
188
+
189
+ return adjacent
190
+
191
+ def find_containing_simplex(self, point: Tuple[float, ...]) -> Optional['SimplexTree']:
192
+ """Finds the leaf simplex containing the given point.
193
+
194
+ Recursively searches the tree to find the smallest (deepest) simplex
195
+ that contains the point.
196
+
197
+ Args:
198
+ point: Coordinates to locate
199
+
200
+ Returns:
201
+ The leaf SimplexTree containing the point, or None if outside tree
202
+ """
203
+ if not self._point_inside_simplex(point):
204
+ return None
205
+
206
+ if self._is_leaf():
207
+ return self
208
+
209
+ for child in self._get_children():
210
+ result = child.find_containing_simplex(point)
211
+ if result is not None:
212
+ return result
213
+
214
+ return self
215
+
216
+ def __repr__(self):
217
+ vertices = self.get_vertices_as_tuples()
218
+ vertex_str = str(vertices)
219
+ return (f"{self.__class__.__name__}(vertices={vertex_str}, "
220
+ f"children={len(self.children)}, depth={self.depth})")
221
+
222
+ def _add_splitting_point(self, point: Tuple[float, ...]) -> List['SimplexTree']:
223
+ if not self._is_leaf():
224
+ for child in self.children:
225
+ if child._point_inside_simplex(point):
226
+ return child._add_splitting_point(point)
227
+
228
+ if not self._point_inside_simplex(point):
229
+ raise ValueError(f"Splitting point {point} is not inside this simplex")
230
+
231
+ self.splitting_point_index = self._get_next_split_index()
232
+ vertex_indices = self.registry.register_vertices([tuple(point)])
233
+ self.splitting_point_vertex_index = vertex_indices[0]
234
+
235
+ vertices = self.get_vertices_as_tuples()
236
+ n_vertices = len(vertices)
237
+
238
+ children = []
239
+ for i in range(n_vertices):
240
+ child_vertices = [v for j, v in enumerate(vertices) if j != i] + [tuple(point)]
241
+ child = self._add_child(child_vertices)
242
+ children.append(child)
243
+ return children
244
+
245
+ def _compute_barycentric_center(self) -> Tuple[float, ...]:
246
+ vertices = self.vertices
247
+ center = np.mean(vertices, axis=0)
248
+ return tuple(float(x) for x in center)
249
+
250
+ def subdivide_leaf(self, leaf: 'SimplexTree') -> List['SimplexTree']:
251
+ """Split a single leaf at its barycenter (used by data-driven subdivision).
252
+
253
+ Args:
254
+ leaf: A leaf node of this tree.
255
+
256
+ Returns:
257
+ The list of freshly created child simplices.
258
+ """
259
+ if not leaf._is_leaf():
260
+ raise ValueError("subdivide_leaf expects a leaf node")
261
+ barycenter = leaf._compute_barycentric_center()
262
+ return leaf._add_splitting_point(barycenter)
263
+
264
+ def _add_barycentric_centers_recursively(self, levels: int) -> None:
265
+ if levels <= 0:
266
+ return
267
+
268
+ if self._is_leaf():
269
+ barycenter = self._compute_barycentric_center()
270
+ self._add_splitting_point(barycenter)
271
+
272
+ for child in self.children:
273
+ child._add_barycentric_centers_recursively(levels - 1)
274
+
275
+ def print_tree(self, show_only_splitting_points: bool = False) -> None:
276
+ """Prints the tree structure to console.
277
+
278
+ Args:
279
+ show_only_splitting_points: If True, shows only splitting point indices.
280
+ If False, shows all nodes with vertex indices.
281
+ """
282
+ def _print(node, prefix: str = "", is_last: bool = True):
283
+ connector = "└── " if is_last else "├── "
284
+ if show_only_splitting_points:
285
+ if node.splitting_point_index is not None:
286
+ print(f"{prefix}{connector}[{node.splitting_point_index}] "
287
+ f"(vertex {node.splitting_point_vertex_index})")
288
+ new_prefix = prefix + (" " if is_last else "│ ")
289
+ child_count = len(node.children)
290
+ for idx, child in enumerate(node.children):
291
+ _print(child, new_prefix, idx == child_count - 1)
292
+ else:
293
+ if node.splitting_point_index is not None:
294
+ label = f"[{node.splitting_point_index}] vertices: {node.vertex_indices}"
295
+ else:
296
+ label = f"vertices: {node.vertex_indices}"
297
+ print(f"{prefix}{connector}{label}")
298
+ new_prefix = prefix + (" " if is_last else "│ ")
299
+ child_count = len(node.children)
300
+ for idx, child in enumerate(node.children):
301
+ _print(child, new_prefix, idx == child_count - 1)
302
+
303
+ _print(self)
304
+
305
+ def get_splitting_points(self) -> List[Tuple[int, Tuple[float, ...]]]:
306
+ """Returns all splitting points currently in the tree.
307
+
308
+ Each splitting point is the center used to subdivide a simplex into
309
+ children.
310
+
311
+ Returns:
312
+ List of ``(split_index, coords)`` tuples for each splitting point
313
+ """
314
+ splitting_points = []
315
+ for node in self._traverse_breadth_first():
316
+ if node.splitting_point_index is not None:
317
+ coords = tuple(self.registry._get_vertex(node.splitting_point_vertex_index))
318
+ splitting_points.append((node.splitting_point_index, coords))
319
+ return splitting_points
@@ -0,0 +1,70 @@
1
+ """Global registry that de-duplicates vertices shared across a simplex tree.
2
+
3
+ Every unique coordinate tuple is stored once and referenced everywhere by an
4
+ integer index. This keeps barycentric feature columns consistent: a given
5
+ column always maps to the same point in space.
6
+ """
7
+
8
+ import numpy as np
9
+ from typing import List, Tuple, Dict
10
+
11
+
12
+ class VertexRegistry:
13
+ def __init__(self, tolerance: float = 1e-10):
14
+ self.vertices: List[np.ndarray] = []
15
+ self.vertex_to_index: Dict[Tuple, int] = {}
16
+ self.tolerance = tolerance
17
+
18
+ def _register_vertex(self, vertex: Tuple[float, ...]) -> int:
19
+ vertex_tuple = tuple(float(x) for x in vertex)
20
+ if vertex_tuple in self.vertex_to_index:
21
+ return self.vertex_to_index[vertex_tuple]
22
+ idx = len(self.vertices)
23
+ self.vertices.append(np.array(vertex_tuple))
24
+ self.vertex_to_index[vertex_tuple] = idx
25
+ return idx
26
+
27
+ def register_vertices(self, vertices: List[Tuple[float, ...]]) -> List[int]:
28
+ """Registers multiple vertices and returns their indices.
29
+
30
+ Existing vertices return their existing index (no duplicates created).
31
+
32
+ Args:
33
+ vertices: List of coordinate tuples to register
34
+
35
+ Returns:
36
+ List of integer indices corresponding to each vertex
37
+ """
38
+ return [self._register_vertex(v) for v in vertices]
39
+
40
+ def _get_vertex(self, idx: int) -> np.ndarray:
41
+ return self.vertices[idx]
42
+
43
+ def _get_vertices(self, indices: List[int]) -> List[np.ndarray]:
44
+ return [self.vertices[idx] for idx in indices]
45
+
46
+ def _get_vertex_as_tuple(self, idx: int) -> Tuple[float, ...]:
47
+ return tuple(float(x) for x in self.vertices[idx])
48
+
49
+ def get_vertices_as_tuples(self, indices: List[int]) -> List[Tuple[float, ...]]:
50
+ """Returns coordinates as tuples for the given vertex indices.
51
+
52
+ Args:
53
+ indices: List of vertex indices to look up
54
+
55
+ Returns:
56
+ List of coordinate tuples
57
+ """
58
+ return [self._get_vertex_as_tuple(idx) for idx in indices]
59
+
60
+ def as_matrix(self) -> np.ndarray:
61
+ """Returns all registered vertices stacked into an (n_vertices, dim) array."""
62
+ if not self.vertices:
63
+ return np.empty((0, 0))
64
+ return np.vstack(self.vertices)
65
+
66
+ def __len__(self) -> int:
67
+ return len(self.vertices)
68
+
69
+ def __repr__(self) -> str:
70
+ return f"VertexRegistry(num_vertices={len(self.vertices)})"
@@ -0,0 +1,102 @@
1
+ """Optional 2-D visualization helpers (requires the ``viz`` extra: matplotlib).
2
+
3
+ These only make sense for 2-dimensional data. Import lazily so the core package
4
+ works without matplotlib installed.
5
+ """
6
+
7
+ from typing import List, Optional, Tuple
8
+
9
+
10
+ def _require_matplotlib():
11
+ try:
12
+ import matplotlib.pyplot as plt # noqa: F401
13
+ import matplotlib.patches as patches # noqa: F401
14
+ except ImportError as exc: # pragma: no cover
15
+ raise ImportError(
16
+ "Visualization requires matplotlib. Install the extra with "
17
+ "`pip install simplex-tree-classifier[viz]`."
18
+ ) from exc
19
+ return plt, patches
20
+
21
+
22
+ def _draw_2d_simplex(vertices, ax, color, alpha=0.3, linewidth=1, s=50, label=None):
23
+ import matplotlib.patches as patches
24
+ n = len(vertices)
25
+ if n == 0:
26
+ return
27
+ if n == 1:
28
+ x, y = vertices[0]
29
+ ax.scatter([x], [y], color=color, s=s * 2, label=label)
30
+ elif n == 2:
31
+ x, y = zip(*vertices)
32
+ ax.plot(x, y, color=color, linewidth=linewidth * 2, alpha=alpha, label=label)
33
+ ax.scatter(x, y, color=color, s=s, alpha=alpha)
34
+ elif n == 3:
35
+ triangle = patches.Polygon(vertices, facecolor=color, alpha=alpha,
36
+ edgecolor="black", linewidth=linewidth)
37
+ ax.add_patch(triangle)
38
+ x, y = zip(*vertices)
39
+ ax.scatter(x, y, color=color, s=s, alpha=alpha, label=label)
40
+ else:
41
+ x, y = zip(*vertices)
42
+ ax.scatter(x, y, color=color, s=s, alpha=alpha, label=label)
43
+ for i in range(n):
44
+ for j in range(i + 1, n):
45
+ ax.plot([vertices[i][0], vertices[j][0]],
46
+ [vertices[i][1], vertices[j][1]],
47
+ color=color, linewidth=linewidth, alpha=alpha)
48
+
49
+
50
+ def _draw_children_recursive(node, ax, colors, depth=0):
51
+ for i, child in enumerate(node._get_children()):
52
+ color = colors[(depth + i) % len(colors)]
53
+ _draw_2d_simplex(child.get_vertices_as_tuples(), ax, color,
54
+ alpha=0.2, linewidth=1, s=30)
55
+ _draw_children_recursive(child, ax, colors, depth + 1)
56
+
57
+
58
+ def visualize_simplex_tree(tree, data_points=None,
59
+ title: str = "Simplex Tree",
60
+ figsize: Tuple[int, int] = (10, 8)):
61
+ """Plot a 2-D simplex tree and (optionally) overlaid data points."""
62
+ plt, _ = _require_matplotlib()
63
+ fig, ax = plt.subplots(figsize=figsize)
64
+ colors = ["red", "blue", "green", "orange", "purple", "brown",
65
+ "pink", "gray", "cyan", "magenta"]
66
+
67
+ _draw_2d_simplex(tree.get_vertices_as_tuples(), ax, "red", alpha=0.3,
68
+ linewidth=2, s=20, label="Root simplex")
69
+ _draw_children_recursive(tree, ax, colors, depth=0)
70
+
71
+ if data_points is not None and len(data_points) > 0:
72
+ xs = [p[0] for p in data_points]
73
+ ys = [p[1] for p in data_points]
74
+ ax.scatter(xs, ys, color="black", s=40, alpha=0.8,
75
+ edgecolors="white", linewidth=1, label="Data", zorder=10)
76
+
77
+ ax.set_xlabel("x1")
78
+ ax.set_ylabel("x2")
79
+ ax.set_title(title)
80
+ ax.set_aspect("equal")
81
+ ax.legend()
82
+ ax.grid(True, alpha=0.3)
83
+ plt.tight_layout()
84
+ return fig, ax
85
+
86
+
87
+ def plot_leaf_boundaries(classifier, ax=None,
88
+ color: str = "red", linewidth: float = 0.5):
89
+ """Draw the edges of every leaf simplex of a fitted 2-D classifier."""
90
+ plt, _ = _require_matplotlib()
91
+ if classifier.dimension != 2:
92
+ raise ValueError("plot_leaf_boundaries only supports 2-D classifiers.")
93
+ if ax is None:
94
+ _, ax = plt.subplots(figsize=(8, 8))
95
+ for boundary in classifier.get_simplex_vertices():
96
+ if len(boundary) >= 3:
97
+ closed = list(boundary) + [boundary[0]]
98
+ xs = [p[0] for p in closed]
99
+ ys = [p[1] for p in closed]
100
+ ax.plot(xs, ys, color=color, linewidth=linewidth, alpha=0.6)
101
+ ax.set_aspect("equal")
102
+ return ax
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: simplex-tree-classifier
3
+ Version: 0.1.0
4
+ Summary: Hierarchical simplex-tree classifier with barycentric embedding, GPU-accelerated transform, and non-convex boundary pruning.
5
+ Author: Yasmin
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/yasmin/SimplexTreeClassifier
8
+ Project-URL: Repository, https://github.com/yasmin/SimplexTreeClassifier
9
+ Keywords: machine-learning,classifier,simplex,barycentric,interpretability,surrogate-model
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy>=1.21
18
+ Requires-Dist: scipy>=1.7
19
+ Requires-Dist: scikit-learn>=1.0
20
+ Requires-Dist: torch>=1.12
21
+ Provides-Extra: viz
22
+ Requires-Dist: matplotlib>=3.4; extra == "viz"
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # simplex-tree-classifier
28
+
29
+ A hierarchical **simplex-tree classifier**. It embeds points into the barycentric
30
+ coordinates of a subdivided simplex, then trains any scikit-learn estimator on
31
+ that sparse, geometry-aware feature space. The barycentric `transform` is
32
+ accelerated with **PyTorch** and runs on the GPU when one is available.
33
+
34
+ It works in two ways:
35
+
36
+ - **Dataset mode** - train directly on your data `(X, y)`.
37
+ - **Surrogate mode** - imitate an existing `model` (e.g. a neural network) to
38
+ produce an interpretable, piecewise-linear geometric surrogate.
39
+
40
+ Everything is **N-dimensional**.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install simplex-tree-classifier # core (numpy, scipy, scikit-learn, torch)
46
+ pip install simplex-tree-classifier[viz] # + matplotlib for 2-D plots
47
+ ```
48
+
49
+ From source:
50
+
51
+ ```bash
52
+ cd simplex_tree_package
53
+ pip install -e .
54
+ ```
55
+
56
+
57
+
58
+ ## Quickstart
59
+
60
+
61
+
62
+ ### Dataset mode
63
+
64
+ ```python
65
+ import numpy as np
66
+ from simplex_tree_classifier import SimplexTreeClassifier
67
+
68
+ X = np.random.uniform(0, 1, size=(500, 4))
69
+ y = (X.sum(axis=1) > 2.0).astype(int)
70
+
71
+ clf = SimplexTreeClassifier(subdivision_levels=2) # default classifier: LinearSVC
72
+ clf.fit(X, y)
73
+ print(clf.predict(X[:5]))
74
+ ```
75
+
76
+
77
+
78
+ ### Surrogate mode (imitate a model)
79
+
80
+ Pass any object with `.predict(X)` or any callable `X -> labels`, plus the input
81
+ dimensionality. Fill points are sampled in `[0, 1]^d` and labeled by the model.
82
+
83
+ ```python
84
+ def black_box(X): # e.g. a wrapped PyTorch model
85
+ X = np.atleast_2d(X)
86
+ return (X[:, 0] + X[:, 1] > 1.0).astype(int)
87
+
88
+ surrogate = SimplexTreeClassifier(
89
+ model=black_box,
90
+ n_features=2,
91
+ subdivision_levels=4,
92
+ n_fill=5000,
93
+ )
94
+ surrogate.fit() # samples + labels internally
95
+ ```
96
+
97
+
98
+
99
+ ### Data-driven subdivision
100
+
101
+ Instead of uniform (barycentric) subdivision, split only where the classifier is
102
+ still wrong:
103
+
104
+ ```python
105
+ clf = SimplexTreeClassifier(
106
+ subdivision_strategy="data_driven",
107
+ subdivision_levels=6, # here: maximum tree depth
108
+ max_leaves=500,
109
+ )
110
+ clf.fit(X, y)
111
+ ```
112
+
113
+
114
+
115
+ ## API overview
116
+
117
+ Constructor: `SimplexTreeClassifier(classifier=None, model=None, n_features=None, subdivision_levels=1, subdivision_strategy="barycentric", n_fill=5000, max_leaves=None, normalize=None, margin=0.05, device=None, tolerance=1e-10, random_state=None, data_driven_max_iter=50)`
118
+
119
+ Core methods:
120
+
121
+ - `transform(X)` - sparse barycentric embedding (GPU-accelerated); rows sum to 1.
122
+ - `fit(X=None, y=None)` - trains the classifier. In surrogate mode `X`/`y` are
123
+ optional. Automatically records same-side simplices afterward (linear only).
124
+ - `predict(X)` - predicted labels.
125
+
126
+ Geometry queries:
127
+
128
+ - `find_containing_simplex(point)` - the leaf simplex a point falls in.
129
+ - `find_adjacent_simplexes(simplex)` - leaves sharing a `(d-1)`-face.
130
+ - `get_simplex_vertices()` - vertices of every leaf simplex (renamed from
131
+ `get_simplex_boundaries`).
132
+ - `is_in_simplex(point, simplex)` - boolean containment test.
133
+
134
+ Decision-boundary analysis (linear classifiers, multiclass-aware):
135
+
136
+ - `identify_crossing_simplices()` - leaves crossed by the decision boundary.
137
+ - `remove_nonconvex_leaves(criterion="distance"|"convexity_sign", removal_factor=0.15, epsilon=None, keep_frac=None, min_depth=0.0, max_remove_frac=0.25, max_iter=10, refit=True)` - iteratively prune leaves whose
138
+ boundary bends non-convexly.
139
+ - `find_same_side_simplices()` - subdivisions that don't touch the boundary (also
140
+ run automatically at the end of `fit`, stored in `same_side_keys_`).
141
+ - `compute_plane_equations()` - the boundary hyperplane within each crossing leaf.
142
+
143
+ Helpers: `SimplexTree`, `Simplex`, `VertexRegistry`, `make_enclosing_simplex(d)`,
144
+ `get_device()`.
145
+
146
+ ## GPU acceleration
147
+
148
+ `transform` stacks each leaf's inverse edge matrix into tensors and embeds an
149
+ entire batch of points against all leaves at once. The device is auto-detected
150
+ (CUDA if present); override with `device="cpu"` / `device="cuda"`. Points outside
151
+ every leaf (or inside a degenerate one) fall back to an exact per-point search.
152
+
153
+ ## TODO
154
+
155
+ - [ ] **Overleaf cross-check.** Reconcile this package's operations against the
156
+ list of supported operations in the paper (Overleaf) and add anything missing.
157
+
@@ -0,0 +1,14 @@
1
+ simplex_tree_classifier/__init__.py,sha256=I7M12scTJVYBzkZfoYw4Rv_Wh4aOJvUC6GsURd66s_I,888
2
+ simplex_tree_classifier/backend.py,sha256=zSS6rZu-3ys5VGraLWMR03za_udmgOBkX-DFM7Yyaiw,7137
3
+ simplex_tree_classifier/classifier.py,sha256=tY-aWwtO_EWkCF6d3Bo3fV5_xc6L5NoTiQdEdI2X6CY,33016
4
+ simplex_tree_classifier/convexity.py,sha256=skg3iYo14iMo5_-6USeVUcLIhU9_bNvPRoPEcAmcuNs,8508
5
+ simplex_tree_classifier/plane_equation.py,sha256=JNfwlLKmYB4tZHRhs4Ly5Hyf074FT2LkBafjHtc7i1I,2481
6
+ simplex_tree_classifier/simplex.py,sha256=SwiM7_ay3DA5kMPKtS7UsF5m1uIkX8ca8JFa2dDg7yg,6208
7
+ simplex_tree_classifier/simplex_tree.py,sha256=Qc8gZUYuYnEloEiRIBpb_FZb3PYSQKjkgpVHZYw6Kto,12762
8
+ simplex_tree_classifier/vertex_registry.py,sha256=UzkgeHaXIm15ZCmglWsKuMKU5lpwJxKEA60ZNwDs3iU,2548
9
+ simplex_tree_classifier/visualization.py,sha256=NvSETvHRQb3S8MWOLyPUKMvFeTebec2TU1MazWzlIUk,4064
10
+ simplex_tree_classifier-0.1.0.dist-info/licenses/LICENSE,sha256=00sYuqcAxSjdor89Kd8thtcvK-xhUXf-fsBR1oTeC14,1084
11
+ simplex_tree_classifier-0.1.0.dist-info/METADATA,sha256=Ky0YZjSiGiD5lGPCqTpO2RwbymHcYanjWlPNeg13mB0,5227
12
+ simplex_tree_classifier-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
13
+ simplex_tree_classifier-0.1.0.dist-info/top_level.txt,sha256=PO4kiKAZRtkgzHgHtzufjkEL55mkhqvsTtF2hK4Wtdc,24
14
+ simplex_tree_classifier-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yasmin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.