Skip to main content

rustures

Fast, memory-aware offline change-point detection for Python — powered by Rust.

Python PyPI Documentation Rust PyO3 License Status

Exact dynamic programming, kernel methods, robust costs, custom Python costs, and familiar fit / predict APIs in one native extension.

Documentation · Quick start · Algorithms · Custom costs · Tutorial


rustures finds points where the statistical behaviour of a sequence changes: its mean, distribution, trend, autoregressive dynamics, or kernel representation. The search algorithms and built-in cost functions run in Rust while the public API stays in Python.

The project is inspired by the excellent ruptures ecosystem, but it is an independent implementation rather than a complete drop-in replacement.

[!IMPORTANT] rustures is currently pre-alpha. The API is usable and heavily tested, but public APIs and compatibility guarantees may still change between releases.

Why rustures?

  • A native core without a Python loop in the hot path. Built-in costs and detectors execute in optimized Rust.
  • Real concurrency from ordinary Python threads. Built-in predictions release the GIL while the native search runs, so independent detections can overlap in a ThreadPoolExecutor without process-level serialization overhead.
  • Exact and approximate search strategies. Use fixed-K dynamic programming, penalized optimal partitioning, kernel CPD, or faster greedy detectors.
  • Memory is part of the API. Dynp and full-Gram kernel backends reject oversized jobs before allocating their main tables.
  • Kernel CPD without mandatory quadratic storage. The default fused backend is exact and does not materialize a full Gram matrix.
  • Endpoint-batched custom Python costs. Dynp and Pelt accept ordinary scalar callbacks plus an optional vectorized error_many(starts, ends) protocol that is not available in ruptures' scalar-only custom-cost interface.
  • Multivariate input is first-class. Most costs accept an (n_samples, n_features) NumPy array; scalar signals may remain one-dimensional.
  • Python-safe failure boundaries. Invalid data, allocation limits, numerical failures, and unwinding Rust panics become catchable Python exceptions.
  • Reproducible validation. Deterministic generators, metrics, exhaustive small oracles, parity fixtures, and raw benchmark artifacts live in the repository.

Quick start

import rustures as rpt

# Deterministic piecewise-constant data and its true breakpoints.
signal, truth = rpt.pw_constant(
    n_samples=600,
    n_features=2,
    n_bkps=3,
    noise_std=0.7,
    seed=42,
)

# Penalized exact segmentation.
prediction = rpt.Pelt(
    model="l2",
    min_size=10,
    jump=1,
).fit_predict(signal, pen=12.0)

precision, recall = rpt.precision_recall(truth, prediction, margin=10)

print("truth:     ", truth)
print("prediction:", prediction)
print(f"precision={precision:.3f}, recall={recall:.3f}")

Breakpoints use the half-open interval convention and always include the terminal sample. A result such as [120, 360, 600] represents segments [0, 120), [120, 360), and [360, 600).

Choosing an algorithm

You know… Start with What it does
The number of changes K Dynp Exact fixed-K dynamic programming
A penalty per additional change Pelt Exact penalized optimal partitioning; uses pruning only when the cost proves it is valid
The change may be nonlinear or distributional KernelCPD Exact linear, RBF, or cosine kernel segmentation
You need a fast exploratory result Binseg Recursive binary segmentation
You prefer merge-based segmentation BottomUp Starts small and merges neighbouring segments
Changes should be found from a local score Window Window discrepancy with deterministic peak selection
A scalar signal is piecewise constant with robust L1 loss L1Potts Weighted scalar L1-Potts optimization

Fixed number of changes

algo = rpt.Dynp(model="normal", min_size=8, jump=2).fit(signal)

print("workspace bytes:", algo.estimated_memory_bytes(n_bkps=3))
breakpoints = algo.predict(n_bkps=3)

Dynp defaults to a 512 MiB prediction-workspace limit. Override it explicitly when you know the process budget:

algo = rpt.Dynp(
    model="l2",
    jump=1,
    max_memory_bytes=256 * 1024 * 1024,
).fit(signal)

# Raises MemoryError before allocating DP states if the limit would be exceeded.
breakpoints = algo.predict(n_bkps=32)

Kernel change-point detection

kernel_algo = rpt.KernelCPD(
    kernel="rbf",
    gamma_policy="sampled",
    gamma_samples=10_000,
    seed=42,
    backend="fused",
    min_size=5,
    jump=1,
)

breakpoints = kernel_algo.fit_predict(signal, n_bkps=3)

Available kernels are "linear", "rbf", and "cosine".

Backend Exact? Main storage behaviour
fused Yes Default fixed-K implementation; no full Gram matrix
streaming Yes Computes kernel contributions without retaining a full Gram table
full Yes Stores a full Gram prefix for repeated constant-time segment-cost queries

The full backend has its own 512 MiB default limit through max_gram_bytes.

Cost models

The following model strings work with the general-purpose detectors:

Model Detects changes in… Notes
l2 Mean Fast prefix sums; scalar or multivariate
l1 Median / robust location Component-wise median absolute deviation
rank Distribution Global ranks with tie handling
normal Gaussian mean and covariance Regularized covariance log-determinant
linear Regression relationship First column is the response; remaining columns are predictors
ar Autoregressive dynamics Default order is 4
clinear Continuous piecewise-linear trend Endpoint interpolation cost
mahalanobis Metric-weighted scatter Exposed as CostMahalanobis(metric=...) / CostMl

Standalone cost objects expose fit, error(start, end), and sum_of_costs:

cost = rpt.CostL2().fit(signal)
segment_cost = cost.error(100, 220)
partition_cost = cost.sum_of_costs([100, 220, len(signal)])

Custom Python costs

Dynp and Pelt accept any object with this protocol:

class CustomCost:
    min_size: int

    def fit(self, signal): ...
    def error(self, start: int, end: int) -> float: ...

    # Optional pairwise batch: result[i] == error(starts[i], ends[i]).
    # This is not a Cartesian product of every start and end.
    def error_many(self, starts, ends): ...

For example, a Bernoulli negative log-likelihood cost can be written as:

import numpy as np
import rustures as rpt


class BernoulliCost:
    min_size = 1

    def fit(self, signal):
        values = np.asarray(signal, dtype=np.float64).reshape(-1)
        if not np.all((values == 0.0) | (values == 1.0)):
            raise ValueError("BernoulliCost expects only 0 and 1")
        self.values = values
        self.prefix = np.r_[0.0, np.cumsum(values)]
        return self

    def error(self, start, end):
        length = end - start
        ones = self.prefix[end] - self.prefix[start]
        p = ones / length
        if p == 0.0 or p == 1.0:
            return 0.0
        return -(ones * np.log(p) + (length - ones) * np.log1p(-p))

    def error_many(self, starts, ends):
        starts = np.asarray(starts, dtype=np.intp)
        ends = np.asarray(ends, dtype=np.intp)
        lengths = ends - starts
        ones = self.prefix[ends] - self.prefix[starts]
        probabilities = ones / lengths
        mixed = (ones > 0.0) & (ones < lengths)
        costs = np.zeros(len(starts), dtype=np.float64)
        costs[mixed] = (
            -ones[mixed] * np.log(probabilities[mixed])
            -(lengths[mixed] - ones[mixed])
            * np.log1p(-probabilities[mixed])
        )
        return costs


binary_signal = np.r_[np.zeros(80), np.ones(60), np.zeros(90)]

breakpoints = rpt.Dynp(
    custom_cost=BernoulliCost(),
    min_size=10,
    jump=1,
).fit_predict(binary_signal, n_bkps=2)

Unlike ruptures' custom-cost protocol, which calls error(start, end) once per candidate, Rustures can send every candidate ending at the current endpoint through one error_many call. This makes NumPy broadcasting and prefix-array indexing possible without storing a full O(n²) cost table. Search algorithms batch only the candidates they currently need, while a standalone segment request evaluates only that segment.

The batch contract is pairwise, not Cartesian. For one-dimensional arrays with shape (m,), the returned float64 array must also have shape (m,) and satisfy costs[i] == error(int(starts[i]), int(ends[i])). During endpoint batching, all entries of ends normally contain the same endpoint:

starts = [0,   4,   8]
ends   = [20, 20, 20]
costs  = [C(0,20), C(4,20), C(8,20)]

error_many must be genuinely vectorized to provide the largest benefit. Wrapping scalar error calls in a Python list comprehension reduces Rust/Python crossings but retains the Python loop. In a local pruned-Pelt workload (N=800, two features, jump=4), a vectorized prefix-L2 callback reduced Rustures' callback count from 6,586 to 203 and predict time from 75.40 ms to 2.96 ms. This 25.5x figure is an internal scalar-versus-vectorized Rustures comparison, not a claim that every custom cost or every workload is 25.5x faster than ruptures. Exceptions raised by a custom cost preserve their Python type, message, and traceback. Custom Pelt uses the exact unpruned path because arbitrary user costs do not automatically satisfy the PELT pruning inequality. A cost whose author has proved the PELT inequality may explicitly expose a finite constant:

class PrunableCustomCost(CustomCost):
    pelt_pruning_constant = 0.0

This is a mathematical correctness promise, not a tuning flag: an invalid value can prune the optimal partition. After fitting, Pelt.uses_pelt_pruning reports whether the optimized path is active.

Included utilities

Deterministic signal generators:

  • pw_constant
  • pw_linear
  • pw_normal
  • pw_wavy

Evaluation metrics:

  • hausdorff
  • precision_recall
  • rand_index

All generators require an explicit seed and return (signal, breakpoints). A seed is reproducible within a Rustures version; generator streams may change between releases when the documented RNG implementation is optimized.

Performance snapshot

The latest integration benchmark used Windows x86-64, Python 3.11, Rustures 0.1.1, ruptures 1.1.10, isolated worker processes, and five warmed timing runs. It exercised 57 cost, detector, kernel, custom-cost, metric, and dataset cases. Rustures returned valid results in every case; 39 of 40 comparable breakpoint results matched exactly. The remaining AR case uses a documented different segment-boundary policy.

Measured group Geometric-mean result versus ruptures
L2 Dynp, four signal families (N=720) 1347.00× faster
L2 Pelt, four signal families (N=1200) 668.77× faster
Fused KernelCPD, linear/RBF/cosine (N=720) 1.37× faster
Full-Gram KernelCPD (N=720) 1.89× slower
Gram-free streaming KernelCPD (N=720) 1.12× slower
Scalar custom Pelt with proven pruning opt-in (N=800) 1.07× slower
Synthetic dataset generators (N=80000) 1.33× faster

The streaming backend incrementally reuses each symmetric kernel pair while retaining only O(n) endpoint state; fused remains the default high-throughput path. These are machine- and workload-specific measurements, not universal guarantees. Raw timing, breakpoint, environment, and process-RSS data is available in artifacts/validation/integration-comparison-optimized-windows-py311.json, and the reproducible driver is benchmarks/integration_comparison.py.

Correctness and safety

The repository currently checks correctness through several independent layers:

  • exhaustive enumeration for small fixed-K and penalized problems;
  • black-box parity fixtures generated from pinned ruptures behaviour;
  • full-Gram, streaming, fused, scalar, and AVX2 backend parity tests;
  • deterministic tie-breaking tests;
  • finite-input, overflow, singular, collinear, constant, and large-offset cases;
  • 2,528 Linear/AR fast-path comparisons against scalar SVD Dynp and Pelt;
  • Python exception and Rust panic-boundary process-survival tests.

The AVX2 path uses runtime CPU detection. CPUs without AVX2 automatically use the scalar implementation instead of failing at import time.

Installation

Current compatibility

  • Python 3.10 or newer is declared through abi3-py310.
  • NumPy 1.23 or newer is required.
  • Binary wheels are published for Linux x86-64 and ARM64, Windows x86-64, and macOS Intel and Apple Silicon.
  • The current wheels target GIL-enabled CPython. They do not target 32-bit Python, PyPy, free-threaded CPython, or native Windows ARM64.

Install a published wheel from PyPI:

python -m pip install rustures

Build from source

Prerequisites: Python 3.10+, Rust 1.83+, and a working native compiler toolchain.

git clone https://github.com/denrew88/rustures.git
cd rustures

python -m venv .venv

Activate the environment:

Windows PowerShell:  .venv\Scripts\Activate.ps1
Linux/macOS:         source .venv/bin/activate

Build and install an editable release extension:

python -m pip install --upgrade pip
python -m pip install "maturin>=1.14,<2.0" "numpy>=1.23"
python -m maturin develop --release

Or create a wheel:

python -m maturin build --release

The wheel is written to target/wheels/.

Development

# Rust unit, oracle, and parity tests
cargo test

# Formatting and linting
cargo fmt -- --check
cargo clippy --all-targets --all-features -- -D warnings

# Python wheel tests after installing a built wheel
python -m pip install pytest
python -m pytest tests/python/test_wheel.py -q

# Longer Linear/AR regression matrix
cargo test --release --test regression_mass -- --ignored --nocapture

Tutorial notebooks are available in English and Korean.

Project status

Implemented today:

  • Dynp, Pelt, Binseg, BottomUp, Window, KernelCPD, and L1Potts
  • eight general-purpose cost models
  • linear, RBF, and cosine kernels with three exact backends
  • custom Python costs for Dynp and Pelt
  • multivariate signal handling, metrics, and deterministic datasets
  • typed Python errors, panic isolation, memory preflight, and type hints

Major work still planned:

  • approximate low-rank kernel backends;
  • broader profiling across CPU architectures and feature dimensions;
  • additional interpreter and architecture coverage as the API matures.

License

Licensed under either of

at your option.

The Rust dependencies compiled into the wheel, their selected license options, copyright notices, and full license texts are recorded in THIRD-PARTY-LICENSES. The report is generated from Cargo.lock for the verified Windows x86-64 target with:

cargo install --locked --features cli cargo-about
cargo about generate --locked --fail -c about.toml -o THIRD-PARTY-LICENSES about.hbs

Built for people who want Python ergonomics, Rust execution, and explicit correctness contracts in offline change-point detection.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rustures-0.2.0.tar.gz (362.0 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

rustures-0.2.0-cp310-abi3-win_amd64.whl (509.0 kB view details)

Uploaded CPython 3.10+Windows x86-64

rustures-0.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (577.5 kB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ x86-64

rustures-0.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (517.5 kB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

rustures-0.2.0-cp310-abi3-macosx_11_0_arm64.whl (502.3 kB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

rustures-0.2.0-cp310-abi3-macosx_10_12_x86_64.whl (542.7 kB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

Details for the file rustures-0.2.0.tar.gz.

File metadata

  • Download URL: rustures-0.2.0.tar.gz
  • Upload date:
  • Size: 362.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for rustures-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d3ef37692f931ffda870e5026f78abb12ec69d7d686053d81ac1378927097b96
MD5 407153460baad3e1ca04b495f6a1fec0
BLAKE2b-256 90dbc3f1a14f6c1e1d6035ae472561977ef7a29b011a4ac52e464b873e39cf6d

See more details on using hashes here.

Provenance

The following attestation bundles were made for rustures-0.2.0.tar.gz:

Publisher: release.yml on denrew88/rustures

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rustures-0.2.0-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: rustures-0.2.0-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 509.0 kB
  • Tags: CPython 3.10+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for rustures-0.2.0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 7d3faede1044299d4eee88f43a9634231217d44bd0679a955afce32524c15894
MD5 d5ef55d638589a00427e47173c65d3ec
BLAKE2b-256 fc4f9027fdba206f27d8e79133203dc9088d5c49362430cdb66caba38daa0643

See more details on using hashes here.

Provenance

The following attestation bundles were made for rustures-0.2.0-cp310-abi3-win_amd64.whl:

Publisher: release.yml on denrew88/rustures

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rustures-0.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for rustures-0.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 080ff2793c1182bf9e0d65fe106df8859908453ff8a8a13d3fdffa3281f70747
MD5 0d13534e630bcf7bf9eff1f2db289fc4
BLAKE2b-256 4d9bcf8b29eeed478e4df964f57da2930df94a74bf436039c385d55d7c5b2512

See more details on using hashes here.

Provenance

The following attestation bundles were made for rustures-0.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on denrew88/rustures

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rustures-0.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for rustures-0.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 2e9a03248a3290c180846a7faa2d9c649a3c23f36d64236765c1c5de2e549ff1
MD5 d61606d3ec68f90f1f937470aeeeb1ee
BLAKE2b-256 4d8546d50e01f632d5a5df5999ff43c58e5311850b4d8046d67215d72bd93d36

See more details on using hashes here.

Provenance

The following attestation bundles were made for rustures-0.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on denrew88/rustures

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rustures-0.2.0-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for rustures-0.2.0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 408b8bb82de69c289b90a5805f81e8b33940aaeb2bb30af6064a4c30bc9dfb2e
MD5 8677be09259039d6d8f198679c944135
BLAKE2b-256 a6186c580af41d48f38070afd72814b2365df36a2bdc85b0ae6c8364c15dc164

See more details on using hashes here.

Provenance

The following attestation bundles were made for rustures-0.2.0-cp310-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on denrew88/rustures

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rustures-0.2.0-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for rustures-0.2.0-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 05c356b3fce3e8fc427660c2f707208559b235aa3beceec2fa304c2a9803a1a3
MD5 5d0f61108ebbef4747f23eb399d76452
BLAKE2b-256 311250c07c42d0dba390e1f3fc68fe27484e7020aef99bc0428992bd16eec97b

See more details on using hashes here.

Provenance

The following attestation bundles were made for rustures-0.2.0-cp310-abi3-macosx_10_12_x86_64.whl:

Publisher: release.yml on denrew88/rustures

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

6 files

0.1.1

6 files

0.1.0

6 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page