Skip to content

Added L1Potts class#344

Open
mstorath wants to merge 4 commits into
deepcharles:masterfrom
mstorath:feature-l1potts
Open

Added L1Potts class#344
mstorath wants to merge 4 commits into
deepcharles:masterfrom
mstorath:feature-l1potts

Conversation

@mstorath
Copy link
Copy Markdown

@mstorath mstorath commented Apr 4, 2025

Summary

Adds L1Potts, an exact O(KN) solver for the (weighted) L1 Potts model
penalized piecewise-constant estimation under L1 data fidelity, robust to
heavy-tailed noise and outliers:

min_u  γ · #{i : u_i ≠ u_{i+1}}  +  Σ_i w_i · |f_i - u_i|

This complements Pelt(model="l1") with a far faster algorithm specialized
for this functional. On a 5000-sample noisy 1D signal: ~20× faster than
Pelt(model="l1", min_size=1, jump=1)
, with identical functional value
to machine precision.

Algorithm

Implements Algorithm 1 of Storath, Weinmann & Unser (2017):

  • Theorem 2 of the paper: the optimum lies in V = unique(signal),
    because the weighted L1 median is always a data value. So the search
    space is reduced from ℝ^N to V^N.
  • Viterbi-style DP over (level, sample) pairs, with the
    Felzenszwalb-Huttenlocher trick for the Potts penalty (O(K) per
    column instead of O(K²)).
  • Time complexity O(K·N), where K is the number of distinct values in the
    signal.

Forward pass matches the paper exactly. Backtracking uses explicit parent
pointers (uint8 matrix + prev-argmin vector)
instead of the paper's
in-place float64 subtraction trick — mathematically equivalent, ~8× less
memory.

Currently 1D-only and penalty-only mode (no n_bkps / epsilon); the
algorithm is exact, so min_size/jump are fixed at 1.

What's included

  • src/ruptures/detection/l1potts.py — solver + input validation
  • tests/test_l1potts.py — 71 tests covering correctness, weights,
    hardening, edge cases:
    • functional-value parity with Pelt+CostL1 over multiple seeds
    • replication-equivalence for integer weights, weight ratios up to
      10000:1
    • positive homogeneity (scaling weights and γ by the same α) over 7
      orders of magnitude
    • reversal symmetry of the functional
    • rejection of NaN/Inf, non-numeric dtypes, empty signals,
      predict-before-fit
    • SyntaxWarning regression net (subprocess + compile())
  • docs/user-guide/detection/l1potts.md — user guide page
  • docs/code-reference/detection/l1potts-reference.md — autodoc stub
  • mkdocs.yml — nav entries
  • Top-level export rpt.L1Potts

Disclosure

Implementation prepared with assistance from Claude (Anthropic) acting as a
coding agent. The algorithm is the cited authors' (SWU2017); the agent's
contributions were the parent-pointer backtracking variant (memory
optimization), input hardening, and the test suite.

@edelweiss611428
Copy link
Copy Markdown

Hi could you submit a feature request to https://github.com/edelweiss611428/rupturesRcpp @mstorath

I may implement this later.

mstorath added a commit to mstorath/ruptures that referenced this pull request May 5, 2026
Fixes pre-commit.ci failure on PR deepcharles#344. v1.7.5's hook manifest used
language: python_venv, which was removed in pre-commit 4.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mstorath and others added 2 commits May 5, 2026 12:23
Penalized change point detection for piecewise constant signals with L1 data fidelity

    The L1 Potts model is defined as follows:

    \min_{u} \sum_{i=1}^N w_i |f_i - u_i| + \gamma \sum_{i=1}^{N-1} \mathbb{I}(u_i \neq u_{i+1})

    The algorithm implemented is described in the paper
    the paper
    Storath, Weinmann, Unser.
    Jump-penalized least absolute values estimation of scalar or circle-valued signals,
    Information and Inference, 2017

The method is significantly faster than PELT for the L1 jump penalized problem.
Rewrite the L1Potts implementation to match Algorithm 1 of
Storath, Weinmann & Unser (2017) explicitly. The forward DP is identical
to the paper; backtracking uses explicit parent pointers (uint8 matrix +
prev-argmin vector) instead of the paper's in-place float64 subtraction
trick. Mathematically equivalent, ~8x less memory.

Hardening: reject non-numeric dtypes, NaN/Inf in signal or weights,
empty signal, and predict-before-fit; clear ValueError/RuntimeError
messages instead of cryptic IndexError/AttributeError. Defensive copies
of signal and weights shield the solver from caller mutation.

Tests (tests/test_l1potts.py, 71 cases):
- functional-value parity with Pelt+CostL1 over 5 random seeds
- replication-equivalence for integer weights, including weight ratios
  up to 10000:1
- positive homogeneity over 7 orders of magnitude in alpha
- reversal symmetry of the functional
- multi-pen independence on a single fit
- edge cases: n=1, K=1, n=2, shape (N,1), pen=+inf, pen=NaN, empty
  signal, complex/object/bool dtypes, list/tuple/read-only inputs
- SyntaxWarning regression net via subprocess + compile()

Docs: user-guide page (docs/user-guide/detection/l1potts.md) with usage
example and reference, code-reference autodoc stub, mkdocs nav entries.
Export L1Potts at top level (rpt.L1Potts).

Performance: on N=5000 noisy 1D signal, ~20x faster than
Pelt(model="l1", min_size=1, jump=1) with identical functional value.

Implementation prepared with assistance from Claude (Anthropic) acting
as a coding agent. The algorithm is the cited authors'; the agent's
contributions were the parent-pointer backtracking variant, input
hardening, and the test suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mstorath mstorath force-pushed the feature-l1potts branch from 4336ef6 to 62bb2f8 Compare May 5, 2026 10:25
mstorath and others added 2 commits May 5, 2026 12:42
pre-commit.ci's runner image upgraded to Python 3.14, where docformatter
v1.7.7's transitive dependency `untokenize` 0.1.1 (sdist from 2013) fails
to build with `AttributeError: 'Constant' object has no attribute 's'`
during `Getting requirements to build wheel`. v1.7.8 dropped the
`untokenize` dependency entirely, fixing the issue.

The original pin comment (`Don't autoupdate until issue deepcharles#293 is fixed`)
referred to the python_venv language manifest issue, which has been fixed
since v1.7.6. The pin has therefore been stale; this PR bumps it forward
by one minor.

Also includes the docstring normalizations v1.7.8 applies (single-line
summary + body convention per PEP 257), keeping pre-commit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
black 25.x and docformatter v1.7.8 fundamentally disagree on the number
of blank lines between a module docstring and a top-level class
definition (PEP 8 wants 2, docformatter v1.7.8 wants 1). The conflict
applies in exactly two files in the repo:

  src/ruptures/exceptions.py
  src/ruptures/metrics/sanity_check.py

These are the only files where a module docstring is directly followed
by a class with no imports in between. All other files have imports
between the docstring and the first class, side-stepping the rule.

Restore those two files to the upstream black-friendly state and add
them to docformatter's exclude regex (alongside the pre-existing
mkdocs_macros.py exclude).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants