Changelog¶
All notable changes to this project are documented here. The format follows Keep a Changelog and the project adheres to Semantic Versioning.
0.1.5 — 2026-07-04¶
Added¶
method="leiden"/method="leiden-cpm"— graph clustering / community detection over the microcluster affinity graph via the full Leiden algorithm (Traag, Waltman & van Eck 2019): local moving → refinement (sub-communities grown from singletons along edges, so each is connected by construction — Leiden's guarantee over Louvain) → aggregation seeded from the pre-refinement partition. It discovers the community count — nok(like the density head). Aresolution(γ) knob trades community count against size; the modularity objective ("leiden", γ = 1 default) has a resolution limit, the CPM objective ("leiden-cpm") is resolution-limit-free (γ on a smaller, density scale). Pure Rust — no eigensolver, NumPy-only. Best for community/blob structure at a moderatethreshold; usemethod="spectral"for elongated manifolds. The self-tuning k-NN affinity graph is shared between the spectral and Leiden heads.betula_cluster.consensus(X, n_clusters, n_runs=…)— clustersXunder several random insertion-order permutations and votes, turning the CF-tree's insertion-order sensitivity (Known Limitation #1) into a measurable quantity: a consensus labelling plus a per-point stability score in[0, 1](ConsensusResult.confidence— low on unstable boundaries, high where every order agrees). NumPy-only; for the partitional heads at a fixedn_clusters. The independent runs parallelize across threads withn_jobs(the Rust core releases the GIL).method="spectral"— spectral clustering over the CF-tree leaf microclusters for non-convex / manifold clusters (moons, rings, spirals) that the centroid heads cannot separate. Self-tuning symmetric k-NN affinity (Zelnik-Manor & Perona local scaling), the normalized Laplacian embedding (Ng-Jordan-Weiss) via the in-house Jacobi eigensolver — no LAPACK/ARPACK, the crate stays NumPy-only — with a k-means landmark reduction above 256 microclusters so theO(m³)solve stays bounded. Dense input only; pair it with a smallthreshold(many leaves) so the microclusters resolve the manifold. No built-in cluster-count selection:n_clusters=0defaults to 2.threshold="auto"for theBetulaestimator — removes the one hyperparameter users most often have to guess. A bounded-subsample pilot fits athreshold=0tree at the samemax_leavesand reads the threshold it converges to, warm-starting the full fit near-converged instead of growing it from zero (fewer rebuild passes, lower peak leaf count on largen). Cached across refits / streaming batches; below the pilot cap it is a no-op (growing from zero is already cheap), and it is dense-only (raises on sparse input).
Changed¶
- Benchmarks now cover every head (spectral, Leiden added to
bench/comprehensive.py) and the compression heads run atmax_leaves = 4000: betula-kmeans is at exact parity with scikit-learn (blobs 0.861 = 0.861) and Ward beats raw Ward while running the fullN. Docs / README / docs site surface the spectral, Leiden and consensus additions; test counts reconciled (190 Python, 158 Rust). The docs site now renders the CHANGELOG and redeploys on every published release.
0.1.4 — 2026-07-04¶
Added¶
MapperGraph.persistence_diagram/MapperGraph.persistence(filtration=…)— 0-D persistent homology of the Mapper nerve by single-linkage union-find (elder rule,O(E log E), pure Rust). Two filtrations:"overlap"(the1 − edge_overlapBhattacharyya gap — a finite bar's death is the depth of a bottleneck, ranking the booleanbridges) and"lens"(the lens sublevel diagram). Essential connected-component classes carryinfdeath.- Greedy weighted k-means++ init (scikit-learn's default): lower-inertia, lower-variance seeds at
~
ln k× the negligible init cost over the leaves. objective="dbcv"fortune— Density-Based Clustering Validation (Moulavi et al. 2014, in[-1, 1]). Unlike the convex Calinski-Harabasz / Davies-Bouldin metrics (which penalise correct non-convex partitions), DBCV validates variable-density / non-convex clusters, so it is the right selection metric for the HDBSCAN-CF and DbStream density heads. NumPy-only, computed over a subsample.
Changed¶
fit_predict_sparse/ the_coreCSR entry points now capn_features(MAX_SPARSE_FEATURES) and validate CSR arrays through the pure-Rustsparse::validate_csr, closing an unbounded-allocation DoS where a hostile caller could force an ~8 EB allocation with a single-nonzero row.- Docs reconciled to the current suite: 172-case Python suite, 147 Rust tests (143 unit + 4
integration under default features; the
python/persistence/clisurfaces add more, 155 total).
Tests¶
- Mutation-testing infrastructure (
cargo-mutantsscoped to the CF math core,mutmutfor the Python wrapper, a weekly non-blocking workflow) plus a CSR-fuzzing proptest and the two coverage gaps it surfaced (the CF-tree absorption boundary, exact tune-metric values).
0.1.3 — 2026-07-04¶
Added¶
betula_cluster.tune— memory-aware hyperparameter search over the CF knobs, scored by an internal metric (Calinski-Harabasz / Davies-Bouldin) or ARI, with a multi-objective quality / memory / speed Pareto mode. NumPy-only by default; an optional Optuna backend (TPE / NSGA-II) viapip install 'betula-cluster[tune]'.- Property-based tests (
proptest, dev-only) for the CF-tree invariants: the clustering feature is a commutative monoid (mergeis associative/commutative and equals a sequential build), folding a tree's leaf features reconstructs the whole-dataset feature, the full-covariance upper-triangular index is a bijection (incl.dim ≥ 4), and the Frequent-Directions sketch is lossless on low-rank data and never overshoots the exact scatter. - Sparse-text benchmark (20 newsgroups, TF-IDF): the
O(nnz)fit_predict_sparsepath and the standard reduce-then-cluster pipeline (TruncatedSVD / NMF → k-means) vs scikit-learn, written up honestly inbench/RESULTS.md(raw high-dTF-IDF concentrates for every fast clusterer; on NMF topics betula matches sklearn). MapperGraph.edge_overlap— a Bhattacharyya coefficient in(0, 1]per Mapper edge, from the pooled diagonal-Gaussian summaries of the two nodes' member microclusters. Surfaced onto_networkx()edges asoverlap=…, so a bridge between well-separated regions reads as a lower-weight edge than one inside a dense blob.- Documentation site (MkDocs Material +
mkdocstringsAPI autodoc, MathJax-rendered math) built fromdocs/, with a GitHub Pages deploy workflow;pip install 'betula-cluster[docs]'for the toolchain.
Changed¶
- Coverage floor (
cargo llvm-cov, ≥95 % lines) now also measures thepersistenceandclifeature sets, not just the default core. - Declared
rust-version = "1.82"(MSRV) and lowered the real floor to it — the streaming heads had an implicit 1.87 dependency (u64::is_multiple_of), now rewritten. AddedDocumentation/Changelogproject URLs. - Docs reconciled to the current suite: 167-case Python suite, 141 Rust tests (137 unit + 4 integration), and five end-to-end use cases (README, DESIGN.md).
- Repository hardening:
macOS/WindowsCI test legs, an sdist install smoke test, a nightlycargo auditcron, Dependabot, andSECURITY.md/CONTRIBUTING.md/ issue templates.
0.1.2 — 2026-06-28¶
Added¶
betula_cluster.__version__, resolved from the installed package metadata.
Changed¶
- README repositioned: compress-then-cluster framing, the test/coverage story surfaced at the top, a
"When to use it" section, and a stable-core / experimental capability split. HDBSCAN is labelled
HDBSCAN-CF consistently in prose (the
method="hdbscan"API string is unchanged).
Fixed¶
- Stale docs: the Python suite is 153 cases (was written as 123);
betula-indexreferences now point tolexindex(the indexing companion's published name).
0.1.1 — 2026-06-28¶
Fixed¶
- PyPI project description: README links to the docs, benchmarks, and examples are now absolute GitHub URLs so they resolve on the PyPI page (relative links only worked in the GitHub-rendered README).
0.1.0 — 2026-06-28¶
First public release.
Added¶
- Numerically stable BETULA clustering features
(n, μ, S)(Welford/Chan updates) with four covariance models: spherical, diagonal, full (PSD via Cholesky), and a Frequent-Directions sketch (O(ℓ·d)per leaf) for very high-dimensional data. - Memory-bounded CF-tree (Phase 1) with auto-rebuild under a
max_leavescap; optional parallel shard+merge build (n_jobs); EWMAdecayfor streaming concept drift. - Global clustering heads: Hamerly-accelerated exact k-means, diagonal & full-covariance GMM-EM
(expected-log E-step + NIW/MAP), Ward-HAC (nearest-neighbour chain), and HDBSCAN-on-CF; automatic
cluster count at
n_clusters=0(BIC / X-means / dendrogram cut). - χ² / Mahalanobis mass-invariant absorption gate (
absorb="chi2"). normalize=Truefor cosine/direction clustering of embeddings (L2-normalized rows on the unit sphere; squared-Euclidean is monotone in cosine). Doubles as the high-dimensional fix: at d≫100 raw Euclidean distances concentrate and the CF-tree collapses, but direction stays discriminative — on MNIST-784 it lifts ARI 0.04 → 0.44, beating scikit-learn (benchmarked inbench/results_real_normalize.csv). Off by default (magnitude is signal on tabular data).- Inline auto-vectorized distance kernels (the compiler vectorizes the tight reductions per call
site;
target-cpu=nativeopts into AVX2 / AVX-512 — see.cargo/config.toml); rayon-parallel labeling. - Python bindings: abi3 wheel (CPython 3.11+), zero-copy NumPy,
float32/float64(no upcast), GIL released during compute; one-shotfit_predictand a scikit-learn-style streamingBetulaestimator (partial_fit/fit/predict/fit_predict). - Full scikit-learn parameter protocol (
get_params/set_params) — works withclone,Pipeline, andGridSearchCV. PEP 561 typed (py.typed+ stubs). - Dataset-structure inspection:
microcluster_centers_/_weights_/_radii_,cluster_centers_/_radii_/_sizes_,outlier_scores,find_outliers,find_near_duplicates,near_duplicate_pairs(scored cosine pairs, exact within each leaf-block — the scalable counterpart to an O(N²) all-pairs scan),sample_representatives,assign_microclusters,summary, andn_rebuilds_/threshold_diagnostics. - Mapper topological skeleton (
topology::mapper→Betula.mapper()→MapperGraph): a lens (density/radius/l2norm/coordinate/eccentricity) over the microclusters, an overlapping cover, per-bin single-linkage at a data-adaptive (median-NN) scale, and a nerve graph with branch points and bridges (Tarjan); optionalto_networkx(). Exploration of structure / RAG leakage / dedup, not a partition.mapper_stability()sweeps the resolution and reports the topology's persistence across scale (β₀ components, β₁ loops, branch points, bridges per resolution). - Soft assignment & confidence:
predict_proba(true posterior for the GMM heads via the per-leaf responsibility matrixmicrocluster_proba_; a documented centroid-distance softmax heuristic for k-means / Ward / HDBSCAN) andassignment_confidence. - Coreset / diagnostics:
export_coreset()→Coreset(leaves as weighted points — a streaming coreset),diagnostics()(compression ratio, radius percentiles, cluster mass spread),representatives(method=medoid|boundary|outlier|diverse), andcluster_profile()(JSON-able geometry for LLM cluster naming). memory_budget_mb: sizemax_leavesfrom a target tree-resident memory (MiB) at fit time instead of tuning it by hand; the resolved value is exposed aseffective_max_leaves_.- Drift monitoring & curation:
snapshot()+Betula.compare_snapshots(before, after)(nearest-centroid match → centroid shifts / mass ratios) andactive_learning_batch(strategy= "uncertain"|"outlier")(rows to review/label). DenStreamstreaming density clusterer (Cao et al., SDM 2006) over fading spherical micro-clusters built on the stable CFs (decay is centroid/radius-invariant);partial_fit/cluster/fit/fit_predict/predict(-1= noise) + microcluster getters, sklearn-style.DbStreamstreaming DBSTREAM clusterer (Hahsler & Bolaños, 2016): fading micro-clusters connected by shared density (faded overlap mass) rather than distance, so it recovers arbitrarily-shaped clusters and keeps close-but-disconnected dense regions apart. Fixed-radius multi-assignment online; offline connects a pair when their overlap mass is≥ alpha·min_weight. Same fading-CF core and sklearn-style API asDenStream;core::stream::DbStreamin Rust.- Streaming quantile sketches (
betula-sketch, insrc/sketch/):KllSketch(Karnin–Lang– Liberty, rank-error) andDdSketch(Masson et al., relative-error) —update/update_many/merge/quantile/quantiles, mergeable, bounded memory. - Sparse input:
fit/fit_predict/partial_fit/predictaccept ascipy.sparsematrix (CSR-routed, rows expanded one at a time — the denseN × dmatrix is never materialized). f64; this dense-tree path keeps the cancellation-free guarantee, computeO(N·d). O(nnz)sparse-native (fit_predict_sparse): one-shot clustering of ascipy.sparsematrix that touches only the non-zeros. Rows summarize into spherical micro-clusters keeping(n, ΣX, ‖ΣX‖², S)(so the mean, cached‖μ‖², and centroid distance areO(nnz)) via a flat leader pass bounded bymax_leaves, then a parametric head (kmeansdefault — robust for high-dsparse) labels each row. Uses the expanded squared-distance form, so unlike the dense path it is not cancellation-free (accurate for sparse rows far from the dense centroid);core::sparse::{summarize_sparse, nearest_sparse}is the Rust API.- Robust insertion (
huber_k): optional Huber/winsorized point updates on the streaming estimator — each point is clamped to withinhuber_kper-dimension standard deviations of its target microcluster before the Welford fold-in, bounding any single point's pull on the centroid (O(k·σ/n)) so stream outliers cannot stretch a centroid or inflate a radius. Off by default; zero-variance dimensions pass through and a 5-point warm-up gates the clip. The result is still a valid(n, μ, S)triple, so every downstream head is unchanged. - Constrained clustering (
must_link/cannot_link): semi-supervised COP-KMeans (Wagstaff et al., 2001) over the leaf microclusters —fit(X, must_link=..., cannot_link=...)/fit_predict(...)take(m, 2)row-index pairs. Must-link is transitively closed; cannot-link is enforced per assignment. Constraints are honoured at the microcluster granularity, so a cannot-link inside one leaf (or contradictory / over-constrained inputs) raisesValueErrorrather than being silently dropped.method="kmeans", dense input;core::clustering::cop_kmeansexposes the Rust API with a typedConstraintError. - Mixed numeric + categorical clustering (
KPrototypes): k-prototypes (Huang, 1997) for mixed data. A mixed CF (MixedCf) pairs the stable numeric(n, μ, S)with a per-attribute category histogram (mode = categorical centroid); distance is‖Δnumeric‖² + γ·(categorical mismatch), withγdefaulting to Huang's heuristic. Rows are leader-summarized into bounded mixed micro-clusters, then clustered. Standalone scikit-learn-style estimator (categoricalcolumn indices,fit/fit_predict/predict,cluster_centroids_/cluster_modes_);core::clustering::{MixedCf, kprototypes, summarize_mixed}is the Rust API. - Command-line interface (
betula, behind theclifeature): a dependency-free binary that clusters a delimited numeric file or stdin and writes one label per row; flags mirror the library (--clusters/--method/--feature/--threshold/ … ;--clusters 0auto-selectsk). save/load+ pickle (joblib-compatible) persistence (serde + CBOR via ciborium, schema-versioned).- NaN/Inf input validation at the boundary.
Fixed¶
estimate_thresholdnow measures the mean nearest-sibling distance within each leaf node (ELKI/BETULA-standard,O(M·capacity)) instead of a global all-pairs scan; the rebuild threshold rises monotonically (no multiplicative bump that compounded across rebuilds and collapsed the tree far belowmax_leaves), and rebuilds reinsert in reverse-DFS leaf order. The CF-tree build is now byte-for-byte the reference (betulars) tree shape and at speed parity with matched build flags.