Skip to main content

l1fit

Fast constrained L1 curve fitting for Python and NumPy. l1fit solves

\min_{x \in \mathbb{R}^n} \; \lVert A x - b \rVert_1
\quad \text{s.t.} \quad C x = d, \;\; E x \le f

with A of size k x n, optional equality constraints C x = d (l rows) and optional inequality constraints E x <= f (m rows), and optional sign restrictions on x and on the residuals. The L1 norm makes the fit robust against outliers: where a least-squares line is pulled towards them, the L1 line ignores them.

The solver is the modified simplex method of Barrodale and Roberts (ACM TOMS algorithm 552, CL1), written in modern Fortran. It works on a single dense tableau, so it needs no LP modelling layer and is much faster than general-purpose solvers on this problem class (see Benchmark). The Python package is a thin, typed nanobind binding that releases the GIL.

Installation

l1fit is available on PyPI:

uv add l1fit        # or: pip install l1fit

There is one wheel per platform for Linux (x86_64, aarch64), macOS (arm64, x86_64) and Windows (x86_64); each covers Python 3.12 to 3.15 (stable ABI). They need nothing but NumPy: the Fortran code uses no runtime library, so there is no libgfortran to bundle. The wheels are built by .github/workflows/wheels.yml.

To build from source (other platforms, or for development) you need a Fortran compiler (gfortran; on Windows the MinGW-w64 one, next to MSVC for the C++), a C++17 compiler and CMake. uv builds the extension with scikit-build-core:

uv sync     # development environment (builds the extension in editable mode)
uv build    # wheel and sdist in dist/

Usage

import numpy as np
from l1fit import Status, solve_l1

# Fit a line through 7 points, one of them an outlier of size 10: the L1 fit ignores it.
t = np.arange(7.0)
b = 1.0 + 2.0 * t
b[3] += 10.0
A = np.column_stack([np.ones(7), t])

result = solve_l1(A, b)
assert result.status is Status.OPTIMAL
print(result.x)  # [1. 2.]
print(result.objective)  # 10.0 (up to rounding) = the sum of the absolute residuals

# Optional equality constraints C x = d, inequality constraints E x <= f and sign restrictions:
result = solve_l1(
    A,
    b,
    C=np.array([[1.0, 1.0]]),
    d=np.array([4.0]),  # x0 + x1 = 4
    E=np.array([[0.0, 1.0]]),
    f=np.array([1.5]),  # x1 <= 1.5
    x_sign=np.array([0, 1]),  # x1 >= 0
)

solve_l1(A, b, C, d, E, f, *, x_sign, residual_sign, tol, max_iter) takes float64 numpy arrays (other dtypes raise TypeError) and never modifies them. (The inequality matrix is called E, as in the paper.) It returns a frozen L1Result with x, residual (b - A x), equality_residual, inequality_slack, objective, status and iterations. Infeasibility, rounding errors and reaching max_iter are reported through result.status (Status.INFEASIBLE, ROUNDING_ERRORS, MAX_ITERATIONS), not as exceptions; invalid arguments raise TypeError or ValueError. NaN and infinity are rejected. The binding releases the GIL, and the solver is reentrant, so threads can solve independent problems in parallel.

The solver works in double precision. The default tolerance is 1e-10 and the default iteration limit 10 * (k + l + m).

Benchmark against CVXPY

CVXPY is the usual way to solve this problem in Python without a dedicated routine. The benchmark (benchmarks/bench_cvxpy.py, instances from tests/instances) uses the natural formulation:

x = cp.Variable(n)
problem = cp.Problem(cp.Minimize(cp.norm(b - A @ x, 1)), [C @ x == d, E @ x <= f])
problem.solve(solver=...)

CVXPY rewrites it as a linear program with one epigraph variable per row (|b_i - A_i x| <= t_i: n + k variables and 2k + m inequality rows) and passes it to a solver. Three were used:

  • HiGHS dual simplex (solver="HIGHS", option solver="simplex") - the same algorithm family as l1fit, on the larger LP;
  • HiGHS interior point (solver="HIGHS", option solver="ipm");
  • Clarabel (solver="CLARABEL"), an interior-point solver for conic problems and CVXPY's default for LPs.

Wall-clock time of the whole call, minimum of 20 runs for l1fit and of 3 for CVXPY (a new cp.Problem for every run, so the times include CVXPY's canonicalization). The factor is the time relative to l1fit:

Instance Size k x n Equality / inequality rows l1fit CVXPY + HiGHS simplex CVXPY + HiGHS IPM CVXPY + Clarabel
500x10 500 x 10 0 / 0 0.4 ms 28.7 ms (72x) 28.2 ms (71x) 11.2 ms (28x)
2000x20 2000 x 20 0 / 0 6.7 ms 582 ms (87x) 393 ms (59x) 87.7 ms (13x)
2000x20_l3_m10 2000 x 20 3 / 10 7.2 ms 516 ms (72x) 354 ms (49x) 93.3 ms (13x)
300x100 300 x 100 0 / 0 8.5 ms 160 ms (19x) 165 ms (19x) 247 ms (29x)
5000x30 5000 x 30 0 / 0 41.5 ms 5255 ms (127x) 2571 ms (62x) 385 ms (9x)
cvxpy_script_1000x250 1000 x 250 10 / 200 139 ms 4549 ms (33x) 3028 ms (22x) 2815 ms (20x)

Apple M1 Pro, Python 3.12.13, NumPy 2.5.3, CVXPY 1.9.3 (HiGHS 1.15.1 through highspy, Clarabel 0.11.1), default solver tolerances, l1fit with its defaults (tol=1e-10). Raw numbers, versions and per-solver details are in benchmarks/results/cvxpy.json; reproduce with

uv run --group bench python benchmarks/bench_cvxpy.py --json benchmarks/results/cvxpy.json

l1fit is 9 to 127 times faster. Even when only the time that the solver itself reports is compared (leaving out CVXPY's modelling overhead), it stays 7 to 124 times faster: the dedicated simplex works on a k x n tableau, while the general LP is much larger. The optimal objectives agree to 1e-12 (HiGHS) and about 1e-9 (Clarabel, whose interior-point tolerance is looser). The comparison is between a specialised and a general-purpose solver on dense random problems (five with a planted feasible point and Student-t noise, cvxpy_script_1000x250 with pure Gaussian data); the factors will differ on other data, and simplex methods slow down on very large or degenerate problems. CVXPY, of course, solves far more than this one problem class.

Background: a modernized CALGO 552

The algorithm is not new: this is a refactored and modernized version of CALGO552.f, the Fortran 77 routine CL1 published in 1980 by I. Barrodale and F. D. K. Roberts (Algorithm 552: Solution of the constrained l1 linear approximation problem, ACM Transactions on Mathematical Software 6(2), 231-235). The original is a single subroutine with 18 arguments, about 60 numeric labels, GOTOs and single precision. The copy this project started from also had a few garbled lines that did not compile; they were repaired first. What was done:

  • Source form and types: fixed form to free-form Fortran 2018 modules (implicit none (type, external), private by default); single precision to double precision (one kind parameter wp, the accumulator dp).
  • Control flow: every GOTO, arithmetic IF and label removed. The single routine was split step by step into small procedures (pivot_tableau, select_entering_column, select_leaving_row, decide_at_optimum, set_up_phase1_costs, ...), each verified before the next one.
  • Interface: assumed-shape contiguous arrays, so four dimension arguments (KLMD, KLM2D, NKLMD, N2D) and the three workspace arrays (CU, IU, S) are gone: the workspace is allocated inside, and 11 arguments are left. Arguments are validated, and failures are reported as status codes (0 optimal, 1 infeasible, 2 rounding errors, 3 iteration limit, 4 invalid input, 5 allocation failed) instead of undefined behaviour. A bind(C) entry point (l1_cl1) serves the Python extension.
  • Behaviour kept: the algorithm itself was not changed. Compiled in single precision, the refactored code reproduced the original bit for bit (status, iterations, solution, residuals) on about 2600 test problems, and it runs at the same speed. Double precision is the only numerical difference: it makes the solver reliable on problems where the single-precision original stops with rounding errors (for example a 1000 x 250 problem with 200 inequality rows).
  • Packaging: the Python extension, wheels for all major platforms, and a test suite that checks every answer against an independent HiGHS reference.

The history of the repository contains the original and the intermediate steps. The algorithm and the original Fortran are the work of Barrodale and Roberts and are credited as such.

Development

uv sync                                  # rebuilds when pyproject.toml, CMakeLists.txt or src/ change
uv run pytest tests                      # tests (about 6 s), see tests/README.md
uv run ruff check && uv run ruff format --check
uv run pyrefly check
uv run python -m nanobind.stubgen -m l1fit._core -o python/l1fit/_core.pyi   # after changing src/_core.cpp
Path Content
python/l1fit/ The typed Python package: solve_l1, L1Result, Status, the generated stub _core.pyi
src/ The native code: the Fortran solver (l1_calgo552.f90 algorithm, l1_c_api.f90 C entry point, l1_precision.f90) and the nanobind binding _core.cpp (about 60 lines: validates shapes, releases the GIL, calls Fortran)
benchmarks/ The CVXPY comparison and its recorded results
scripts/ check_installed_wheel.py, run on every built wheel
tests/ The test suite and the benchmark instances (tests/README.md)

Ruff and pyrefly check the Python package only (python/); Fortitude lints the Fortran (src/); see pyproject.toml and fortitude.toml.

License

BSD 3-Clause, see LICENSE.

Metadata

Release files for l1fit 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for l1fit 0.1.1
File Size Uploaded
l1fit-0.1.1.tar.gz 35.1 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for l1fit 0.1.1
File
l1fit-0.1.1-cp312-abi3-win_amd64.whl CPython 3.12 abi3 Windows x86-64 Details
l1fit-0.1.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl CPython 3.12 abi3 Linux glibc 2.27+ x86-64, Linux glibc 2.28+ x86-64 Details
l1fit-0.1.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl CPython 3.12 abi3 Linux glibc 2.28+ ARM64, Linux glibc 2.26+ ARM64 Details
l1fit-0.1.1-cp312-abi3-macosx_11_0_arm64.whl CPython 3.12 abi3 macOS 11.0+ ARM64 Details
l1fit-0.1.1-cp312-abi3-macosx_10_9_x86_64.whl CPython 3.12 abi3 macOS 10.9+ x86-64 Details

Total release size: 375.0 kB

Release files / l1fit-0.1.1.tar.gz

Download URL l1fit-0.1.1.tar.gz
Size 35.1 kB
Tags Source
SHA-256 checksum
How to use checksums
1dc728c30dfeffd81a813a27d8487552984c66a6d320555107974bd8f8355c29
BLAKE2b-256 checksum
How to use checksums
280d506f9fe737bf40066e0f4c9f2f5731d77046024c1bec2c494cf781d9676b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / l1fit-0.1.1-cp312-abi3-win_amd64.whl

Download URL l1fit-0.1.1-cp312-abi3-win_amd64.whl
Size 91.4 kB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
a0704b1f1fdac560f6f2c200bd307acbc586f30d010150321b329d22ecd00cc0
BLAKE2b-256 checksum
How to use checksums
4a526bad365edce280d9f59520f0249906e0574583935106e86dfb7bf085dcbe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / l1fit-0.1.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl

Download URL l1fit-0.1.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Size 67.3 kB
Tags CPython 3.12 Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 abi3
SHA-256 checksum
How to use checksums
29d21fb13ebf784e004820a459173be856c0f4412d7b947592e2637e42592f9b
BLAKE2b-256 checksum
How to use checksums
b6b298a85f58b13298bdfbda2499702611342c6641e9bfef78c3e512a99d8db7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / l1fit-0.1.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl

Download URL l1fit-0.1.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl
Size 64.2 kB
Tags CPython 3.12 Linux glibc 2.26+ ARM64 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
f467a7fc499f751eb8db00929c358d8c8a9ac3766a44a3b72c90c1285b6c6d8c
BLAKE2b-256 checksum
How to use checksums
d111f8201ecb70f521b51025d53c225f3f2b32fcf01bb125046e39543d1237e1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / l1fit-0.1.1-cp312-abi3-macosx_11_0_arm64.whl

Download URL l1fit-0.1.1-cp312-abi3-macosx_11_0_arm64.whl
Size 57.7 kB
Tags CPython 3.12 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
edc8d18c8b038fac48b44ff62e3f01f01a40e55c9e0eb4ce4eabead6a9ee36df
BLAKE2b-256 checksum
How to use checksums
7f22ba56050b10260e1f0e060a117bc5dc7d7eb4d0ae2cb394458153f9001659
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / l1fit-0.1.1-cp312-abi3-macosx_10_9_x86_64.whl

Download URL l1fit-0.1.1-cp312-abi3-macosx_10_9_x86_64.whl
Size 59.4 kB
Tags CPython 3.12 abi3 macOS 10.9+ x86-64
SHA-256 checksum
How to use checksums
7ddecbf2615b811320d1e1b9f78605304db1ea029fa1a8aaa42b480e30de0cfb
BLAKE2b-256 checksum
How to use checksums
1c2133decb15048c39a34146033adc853f8f16027969a844d5144e3548ecc052
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

6 release files

0.1.0

6 release 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