simplex-tree-classifier 0.1.0__tar.gz → 0.2.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.
Files changed (20) hide show
  1. {simplex_tree_classifier-0.1.0/src/simplex_tree_classifier.egg-info → simplex_tree_classifier-0.2.0}/PKG-INFO +1 -1
  2. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/pyproject.toml +1 -1
  3. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier/__init__.py +1 -1
  4. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier/classifier.py +138 -6
  5. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0/src/simplex_tree_classifier.egg-info}/PKG-INFO +1 -1
  6. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier.egg-info/SOURCES.txt +1 -2
  7. simplex_tree_classifier-0.1.0/tests/test_api.py +0 -249
  8. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/LICENSE +0 -0
  9. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/README.md +0 -0
  10. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/setup.cfg +0 -0
  11. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier/backend.py +0 -0
  12. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier/convexity.py +0 -0
  13. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier/plane_equation.py +0 -0
  14. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier/simplex.py +0 -0
  15. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier/simplex_tree.py +0 -0
  16. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier/vertex_registry.py +0 -0
  17. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier/visualization.py +0 -0
  18. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier.egg-info/dependency_links.txt +0 -0
  19. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier.egg-info/requires.txt +0 -0
  20. {simplex_tree_classifier-0.1.0 → simplex_tree_classifier-0.2.0}/src/simplex_tree_classifier.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simplex-tree-classifier
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Hierarchical simplex-tree classifier with barycentric embedding, GPU-accelerated transform, and non-convex boundary pruning.
5
5
  Author: Yasmin
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simplex-tree-classifier"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Hierarchical simplex-tree classifier with barycentric embedding, GPU-accelerated transform, and non-convex boundary pruning."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -14,7 +14,7 @@ from .plane_equation import PlaneEquation
14
14
  from .classifier import SimplexTreeClassifier
15
15
  from .backend import get_device
16
16
 
17
- __version__ = "0.1.0"
17
+ __version__ = "0.2.0"
18
18
 
19
19
  __all__ = [
20
20
  "SimplexTreeClassifier",
@@ -34,6 +34,12 @@ from .convexity import (
34
34
 
35
35
 
36
36
  class SimplexTreeClassifier:
37
+ # Class-level defaults so instances unpickled from an older version (whose
38
+ # __dict__ predates these attributes) resolve them to None instead of
39
+ # raising AttributeError on access.
40
+ _X_fit: Optional[np.ndarray] = None
41
+ _y_fit = None
42
+
37
43
  def __init__(self,
38
44
  classifier=None,
39
45
  model=None,
@@ -113,6 +119,12 @@ class SimplexTreeClassifier:
113
119
  self._min = None
114
120
  self._max = None
115
121
 
122
+ # The raw (pre-normalization) data actually used to fit. In surrogate
123
+ # mode this is the sampled fill set (plus any points passed to fit),
124
+ # each labeled by the imitated model. Exposed via ``training_data_``.
125
+ self._X_fit: Optional[np.ndarray] = None
126
+ self._y_fit = None
127
+
116
128
  # Result of the automatic same-side pass at the end of fit().
117
129
  self.same_side_keys_: Set[frozenset] = set()
118
130
 
@@ -185,10 +197,30 @@ class SimplexTreeClassifier:
185
197
  return X
186
198
  return (X - self._min) / (self._max - self._min + 1e-10)
187
199
 
200
+ @staticmethod
201
+ def _ensure_in_unit_cube(X: np.ndarray, tol: float = 1e-6) -> None:
202
+ """Validate that inputs are normalized to ``[0, 1]`` per feature.
203
+
204
+ Raises ``ValueError`` when normalization is disabled but the data lies
205
+ outside the unit cube (so the caller either scales the data or passes
206
+ ``normalize=True``).
207
+ """
208
+ if X.size == 0:
209
+ return
210
+ lo = float(np.min(X))
211
+ hi = float(np.max(X))
212
+ if lo < -tol or hi > 1.0 + tol:
213
+ raise ValueError(
214
+ f"Input features must be normalized to [0, 1] when normalize=False "
215
+ f"(got value range [{lo:.4g}, {hi:.4g}]). Either min-max scale your "
216
+ f"data to [0, 1] first, or construct with normalize=True to let the "
217
+ f"classifier scale it for you."
218
+ )
219
+
188
220
  # ------------------------------------------------------------------
189
221
  # Transform
190
222
  # ------------------------------------------------------------------
191
- def transform(self, data_points) -> csr_matrix:
223
+ def transform(self, data_points) -> csr_matrix: # TODO: use pytorch insead of matrix_csr
192
224
  """Embed points into sparse barycentric coordinates.
193
225
 
194
226
  For each point, finds its containing leaf simplex and writes that
@@ -295,6 +327,11 @@ class SimplexTreeClassifier:
295
327
  optional - fill points are sampled and labeled by the model; any ``X``
296
328
  given is added to the fill set.
297
329
 
330
+ For linear classifiers, redundant same-side subdivisions are merged back
331
+ automatically at the end (see ``remove_same_side_leaves``): this shrinks
332
+ the tree without moving the decision boundary. Non-convex pruning stays a
333
+ separate, explicit ``remove_nonconvex_leaves`` call.
334
+
298
335
  Returns:
299
336
  ``self``.
300
337
  """
@@ -308,6 +345,15 @@ class SimplexTreeClassifier:
308
345
  X_fit = X_fit.reshape(1, -1)
309
346
  y_fit = None if y is None else np.asarray(y)
310
347
 
348
+ # When normalization is off, the data must already live in [0, 1]^d,
349
+ # otherwise points fall outside the enclosing simplex and embed to
350
+ # nothing. Fail loudly instead of silently producing empty rows.
351
+ if not self._normalize_enabled():
352
+ self._ensure_in_unit_cube(X_fit)
353
+
354
+ self._X_fit = X_fit
355
+ self._y_fit = y_fit
356
+
311
357
  d = X_fit.shape[1]
312
358
  self._fit_scaler(X_fit)
313
359
  X_norm = self._apply_scaler(X_fit)
@@ -322,9 +368,37 @@ class SimplexTreeClassifier:
322
368
  X_transformed = self.transform(X_norm)
323
369
  self._fit_estimator(X_transformed, y_fit)
324
370
 
325
- self._finalize_fit()
371
+ # Merge back redundant same-side subdivisions (linear only): splits the
372
+ # boundary never crosses and whose leaves are all one class carry no
373
+ # boundary information, so collapsing them removes leaves without moving
374
+ # the decision surface. remove_same_side_leaves() refits and records the
375
+ # final (empty on convergence) same_side_keys_ itself. For non-linear
376
+ # classifiers there is nothing to merge, so we just record the state.
377
+ if self.is_linear_classifier:
378
+ self.remove_same_side_leaves()
379
+ else:
380
+ self._finalize_fit()
326
381
  return self
327
382
 
383
+ @property
384
+ def training_data_(self):
385
+ """The ``(X, y)`` actually used to fit the classifier.
386
+
387
+ In surrogate mode this is the sampled fill set in ``[0, 1]^d`` (plus any
388
+ points passed to ``fit``), each labeled by the imitated model - i.e. the
389
+ surrogate training set the package built for you. In dataset mode it is
390
+ the raw ``X`` (and ``y``) you passed. Raises ``AttributeError`` if the
391
+ classifier has not been fitted yet.
392
+ """
393
+ if self._X_fit is None:
394
+ raise AttributeError(
395
+ "training_data_ is unavailable: the classifier was not fitted, or "
396
+ "it was built by an older version of the package (e.g. a stale "
397
+ "Jupyter kernel or an old cached pickle). Restart the kernel so the "
398
+ "updated package is re-imported, delete the cached model, then refit."
399
+ )
400
+ return self._X_fit, self._y_fit
401
+
328
402
  def _fit_data_driven(self, X_norm: np.ndarray, y) -> None:
329
403
  d = X_norm.shape[1]
330
404
  # Start from the root simplex (no uniform subdivision) and grow where
@@ -562,10 +636,10 @@ class SimplexTreeClassifier:
562
636
  def find_same_side_simplices(self) -> Set[frozenset]:
563
637
  """Find leaf simplices whose siblings all lie on the same boundary side.
564
638
 
565
- These subdivisions do not contribute to the decision boundary and could
566
- be merged back into their parent. Called automatically at the end of
567
- ``fit`` for linear classifiers (stored in ``same_side_keys_``); also
568
- available directly.
639
+ These subdivisions do not contribute to the decision boundary and can be
640
+ merged back into their parent. They are detected *and* merged away
641
+ automatically at the end of ``fit`` for linear classifiers (see
642
+ ``remove_same_side_leaves``); this method is also available directly.
569
643
  """
570
644
  weights, intercept = self._get_weights_and_intercept()
571
645
  same_side_keys: Set[frozenset] = set()
@@ -582,6 +656,64 @@ class SimplexTreeClassifier:
582
656
  same_side_keys.add(frozenset(child.vertex_indices))
583
657
  return same_side_keys
584
658
 
659
+ def remove_same_side_leaves(self, max_iter: int = 50, refit: bool = True) -> int:
660
+ """Merge back redundant same-side subdivisions (linear classifiers only).
661
+
662
+ A parent split is *same-side* when the decision boundary crosses none of
663
+ its (leaf) children and they all fall on the same side of it. Such a
664
+ split adds no information about the boundary, so collapsing it removes
665
+ leaves without moving the decision surface. Detection is repeated,
666
+ refitting the classifier between passes, until no same-side splits
667
+ remain (a new pass can expose parents that only became "all-leaf" after
668
+ an earlier collapse). Called automatically at the end of ``fit``.
669
+
670
+ Args:
671
+ max_iter: Safety cap on the number of detect/collapse passes.
672
+ refit: Whether to refit the classifier on the stored training data
673
+ after each collapse pass.
674
+
675
+ Returns:
676
+ Total number of leaves removed (0 for non-linear classifiers).
677
+ """
678
+ if not self.is_linear_classifier:
679
+ self.same_side_keys_ = set()
680
+ return 0
681
+ self._ensure_synced()
682
+ total = 0
683
+ try:
684
+ keys = self.find_same_side_simplices()
685
+ except Exception:
686
+ self.same_side_keys_ = set()
687
+ return 0
688
+ for _ in range(max_iter):
689
+ if not keys:
690
+ break
691
+ start = len(self.tree.get_leaves())
692
+ for key in keys:
693
+ # Collapsing one child un-splits the whole parent; the remaining
694
+ # sibling keys for that parent then no-op harmlessly.
695
+ self.tree.remove_by_leaf_key(key)
696
+ removed = start - len(self.tree.get_leaves())
697
+ if removed == 0:
698
+ break
699
+ total += removed
700
+ self._mark_dirty()
701
+ self._sync()
702
+ if refit and self._X_train_norm is not None and self._y_train is not None:
703
+ X_transformed = self.transform(self._X_train_norm)
704
+ self._fit_estimator(X_transformed, self._y_train)
705
+ # Re-detect on the new (pruned + refit) tree; this doubles as both the
706
+ # next pass's work-list and the final same-side record, so we never
707
+ # recompute find_same_side_simplices() redundantly afterwards.
708
+ try:
709
+ keys = self.find_same_side_simplices()
710
+ except Exception:
711
+ keys = set()
712
+ break
713
+ # keys already reflects the current tree/weights (empty on convergence).
714
+ self.same_side_keys_ = keys
715
+ return total
716
+
585
717
  def _sampling_epsilon(self, epsilon=None) -> float:
586
718
  """Boundary test-point placement fraction, defaulting to ``1 / (d + 1)``."""
587
719
  if epsilon is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simplex-tree-classifier
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Hierarchical simplex-tree classifier with barycentric embedding, GPU-accelerated transform, and non-convex boundary pruning.
5
5
  Author: Yasmin
6
6
  License-Expression: MIT
@@ -14,5 +14,4 @@ src/simplex_tree_classifier.egg-info/PKG-INFO
14
14
  src/simplex_tree_classifier.egg-info/SOURCES.txt
15
15
  src/simplex_tree_classifier.egg-info/dependency_links.txt
16
16
  src/simplex_tree_classifier.egg-info/requires.txt
17
- src/simplex_tree_classifier.egg-info/top_level.txt
18
- tests/test_api.py
17
+ src/simplex_tree_classifier.egg-info/top_level.txt
@@ -1,249 +0,0 @@
1
- """Smoke tests for the public SimplexTreeClassifier API.
2
-
3
- Run with: pytest simplex_tree_package/tests
4
- """
5
-
6
- import numpy as np
7
- import pytest
8
- from scipy.sparse import issparse
9
- from sklearn.svm import LinearSVC, SVC
10
-
11
- from simplex_tree_classifier import (
12
- SimplexTreeClassifier,
13
- SimplexTree,
14
- Simplex,
15
- make_enclosing_simplex,
16
- get_device,
17
- )
18
-
19
-
20
- def _toy_2d(n=200, seed=0):
21
- rng = np.random.default_rng(seed)
22
- X = rng.uniform(0, 1, size=(n, 2))
23
- y = (X[:, 0] + X[:, 1] > 1.0).astype(int)
24
- return X, y
25
-
26
-
27
- def _toy_nd(n=300, d=5, seed=1):
28
- rng = np.random.default_rng(seed)
29
- X = rng.uniform(0, 1, size=(n, d))
30
- y = (X.sum(axis=1) > d / 2.0).astype(int)
31
- return X, y
32
-
33
-
34
- # ---------------------------------------------------------------------------
35
- # Geometry primitives
36
- # ---------------------------------------------------------------------------
37
-
38
- def test_make_enclosing_simplex_2d_matches_classic_triangle():
39
- verts = make_enclosing_simplex(2, margin=0.0)
40
- assert verts == [(0.0, 0.0), (2.0, 0.0), (0.0, 2.0)]
41
-
42
-
43
- def test_make_enclosing_simplex_contains_unit_cube_corner():
44
- d = 4
45
- verts = make_enclosing_simplex(d) # default margin
46
- tree = SimplexTree(verts)
47
- # Farthest cube corner (1,1,1,1) must be strictly inside.
48
- assert tree._point_inside_simplex(tuple([1.0] * d))
49
-
50
-
51
- def test_get_device_returns_torch_device():
52
- dev = get_device()
53
- assert dev.type in ("cpu", "cuda")
54
-
55
-
56
- # ---------------------------------------------------------------------------
57
- # Constructor modes
58
- # ---------------------------------------------------------------------------
59
-
60
- def test_constructor_dataset_mode_defaults_to_linear_svc():
61
- clf = SimplexTreeClassifier(subdivision_levels=2)
62
- assert isinstance(clf.classifier, LinearSVC)
63
-
64
-
65
- def test_constructor_surrogate_requires_n_features():
66
- with pytest.raises(ValueError):
67
- SimplexTreeClassifier(model=lambda X: np.zeros(len(X)))
68
-
69
-
70
- # ---------------------------------------------------------------------------
71
- # transform / fit / predict
72
- # ---------------------------------------------------------------------------
73
-
74
- def test_transform_rows_sum_to_one_2d():
75
- X, _ = _toy_2d()
76
- clf = SimplexTreeClassifier(subdivision_levels=2)
77
- T = clf.transform(X)
78
- assert issparse(T)
79
- assert T.shape[0] == len(X)
80
- row_sums = np.asarray(T.sum(axis=1)).ravel()
81
- assert np.allclose(row_sums, 1.0, atol=1e-6)
82
-
83
-
84
- def test_fit_predict_2d_reasonable_accuracy():
85
- X, y = _toy_2d()
86
- clf = SimplexTreeClassifier(subdivision_levels=3)
87
- clf.fit(X, y)
88
- preds = clf.predict(X)
89
- assert preds.shape == y.shape
90
- assert (preds == y).mean() > 0.85
91
-
92
-
93
- def test_fit_predict_nd():
94
- X, y = _toy_nd(d=5)
95
- clf = SimplexTreeClassifier(subdivision_levels=2)
96
- clf.fit(X, y)
97
- preds = clf.predict(X)
98
- assert preds.shape == y.shape
99
- assert (preds == y).mean() > 0.7
100
-
101
-
102
- def test_transform_matches_cpu_reference():
103
- """GPU/backend embedding must agree with the exact per-simplex embedding."""
104
- X, _ = _toy_2d(n=50)
105
- clf = SimplexTreeClassifier(subdivision_levels=2)
106
- clf._ensure_tree(2)
107
- T = clf.transform(X).toarray()
108
- for i, point in enumerate(X):
109
- leaf = clf.tree.find_containing_simplex(tuple(point))
110
- emb = leaf._embed_point(tuple(point))
111
- expected = np.zeros(len(clf.tree.registry))
112
- for k, vid in enumerate(leaf.vertex_indices):
113
- expected[vid] = emb[k]
114
- assert np.allclose(T[i], expected, atol=1e-6)
115
-
116
-
117
- # ---------------------------------------------------------------------------
118
- # Data-driven subdivision
119
- # ---------------------------------------------------------------------------
120
-
121
- def test_data_driven_subdivision_grows_tree():
122
- X, y = _toy_2d(n=300)
123
- clf = SimplexTreeClassifier(subdivision_levels=6,
124
- subdivision_strategy="data_driven",
125
- max_leaves=200)
126
- clf.fit(X, y)
127
- assert len(clf.leaf_simplexes) > 1
128
- assert (clf.predict(X) == y).mean() > 0.85
129
-
130
-
131
- # ---------------------------------------------------------------------------
132
- # Surrogate mode
133
- # ---------------------------------------------------------------------------
134
-
135
- def test_surrogate_mode_imitates_callable_model():
136
- def model(X):
137
- X = np.atleast_2d(X)
138
- return (X[:, 0] + X[:, 1] > 1.0).astype(int)
139
-
140
- clf = SimplexTreeClassifier(model=model, n_features=2,
141
- subdivision_levels=3, n_fill=1500,
142
- random_state=0)
143
- clf.fit()
144
- grid = np.random.default_rng(2).uniform(0, 1, size=(200, 2))
145
- agreement = (clf.predict(grid) == model(grid)).mean()
146
- assert agreement > 0.85
147
-
148
-
149
- def test_surrogate_mode_accepts_predict_object():
150
- class Model:
151
- def predict(self, X):
152
- X = np.atleast_2d(X)
153
- return (X[:, 0] > 0.5).astype(int)
154
-
155
- clf = SimplexTreeClassifier(model=Model(), n_features=2,
156
- subdivision_levels=3, random_state=0)
157
- clf.fit()
158
- assert clf.predict(np.array([[0.9, 0.1], [0.1, 0.9]])).shape == (2,)
159
-
160
-
161
- # ---------------------------------------------------------------------------
162
- # Geometry queries / renamed & new methods
163
- # ---------------------------------------------------------------------------
164
-
165
- def test_get_simplex_vertices_returns_leaf_vertices():
166
- clf = SimplexTreeClassifier(subdivision_levels=2)
167
- clf._ensure_tree(2)
168
- verts = clf.get_simplex_vertices()
169
- assert len(verts) == len(clf.leaf_simplexes)
170
- assert all(len(v) == 3 for v in verts) # 2-D leaves are triangles
171
-
172
-
173
- def test_find_containing_simplex_and_is_in_simplex():
174
- clf = SimplexTreeClassifier(subdivision_levels=2)
175
- clf._ensure_tree(2)
176
- point = (0.3, 0.3)
177
- leaf = clf.find_containing_simplex(point)
178
- assert leaf is not None
179
- assert clf.is_in_simplex(point, leaf) is True
180
- # A point far outside the leaf should not be contained.
181
- far = tuple(np.array(leaf.get_vertices_as_tuples()).mean(axis=0) + 100.0)
182
- assert clf.is_in_simplex(far, leaf) is False
183
-
184
-
185
- def test_find_adjacent_simplexes_share_a_face():
186
- clf = SimplexTreeClassifier(subdivision_levels=2)
187
- clf._ensure_tree(2)
188
- leaf = clf.leaf_simplexes[0]
189
- neighbors = clf.find_adjacent_simplexes(leaf)
190
- for nb in neighbors:
191
- shared = set(leaf.vertex_indices).intersection(nb.vertex_indices)
192
- assert len(shared) >= leaf.dimension
193
-
194
-
195
- # ---------------------------------------------------------------------------
196
- # Decision-boundary analysis
197
- # ---------------------------------------------------------------------------
198
-
199
- def test_identify_crossing_simplices_linear():
200
- X, y = _toy_2d()
201
- clf = SimplexTreeClassifier(subdivision_levels=3)
202
- clf.fit(X, y)
203
- crossing = clf.identify_crossing_simplices()
204
- assert isinstance(crossing, list)
205
- assert len(crossing) > 0
206
- assert "simplex" in crossing[0] and "vertices" in crossing[0]
207
-
208
-
209
- def test_identify_crossing_simplices_nonlinear_fallback():
210
- X, y = _toy_2d()
211
- clf = SimplexTreeClassifier(classifier=SVC(kernel="rbf", C=10),
212
- subdivision_levels=3)
213
- clf.fit(X, y)
214
- crossing = clf.identify_crossing_simplices()
215
- assert isinstance(crossing, list)
216
-
217
-
218
- def test_same_side_computed_automatically_after_fit():
219
- X, y = _toy_2d()
220
- clf = SimplexTreeClassifier(subdivision_levels=3)
221
- clf.fit(X, y)
222
- # Attribute exists and is a set (guarded to linear classifiers).
223
- assert isinstance(clf.same_side_keys_, set)
224
-
225
-
226
- # ---------------------------------------------------------------------------
227
- # Non-convex pruning
228
- # ---------------------------------------------------------------------------
229
-
230
- def test_remove_nonconvex_leaves_reduces_or_keeps_leaf_count():
231
- X, y = _toy_2d(n=400)
232
- clf = SimplexTreeClassifier(subdivision_levels=4)
233
- clf.fit(X, y)
234
- before = len(clf.tree.get_leaves())
235
- removed = clf.remove_nonconvex_leaves(criterion="distance",
236
- removal_factor=0.1,
237
- max_remove_frac=0.25)
238
- after = len(clf.tree.get_leaves())
239
- assert removed >= 0
240
- assert after == before - removed
241
-
242
-
243
- def test_remove_nonconvex_requires_linear_classifier():
244
- X, y = _toy_2d()
245
- clf = SimplexTreeClassifier(classifier=SVC(kernel="rbf"),
246
- subdivision_levels=2)
247
- clf.fit(X, y)
248
- with pytest.raises(AttributeError):
249
- clf.remove_nonconvex_leaves()