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.5 ms 29.6 ms (58x) 29.1 ms (57x) 11.4 ms (22x)
2000x20 2000 x 20 0 / 0 8.6 ms 591 ms (68x) 403 ms (47x) 90.0 ms (10x)
2000x20_l3_m10 2000 x 20 3 / 10 9.8 ms 520 ms (53x) 363 ms (37x) 95.7 ms (10x)
300x100 300 x 100 0 / 0 9.9 ms 166 ms (17x) 170 ms (17x) 218 ms (22x)
5000x30 5000 x 30 0 / 0 60.5 ms 5283 ms (87x) 2673 ms (44x) 399 ms (7x)
cvxpy_script_1000x250 1000 x 250 10 / 200 162 ms 4514 ms (28x) 3025 ms (19x) 2586 ms (16x)

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 7 to 87 times faster. Even when only the time that the solver itself reports is compared (leaving out CVXPY's modelling overhead), it stays 5 to 86 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 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.0

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.0
File Size Uploaded
l1fit-0.1.0.tar.gz 35.0 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for l1fit 0.1.0
File
l1fit-0.1.0-cp312-abi3-win_amd64.whl CPython 3.12 abi3 Windows x86-64 Details
l1fit-0.1.0-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.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl CPython 3.12 abi3 Linux glibc 2.26+ ARM64, Linux glibc 2.28+ ARM64 Details
l1fit-0.1.0-cp312-abi3-macosx_11_0_arm64.whl CPython 3.12 abi3 macOS 11.0+ ARM64 Details
l1fit-0.1.0-cp312-abi3-macosx_10_9_x86_64.whl CPython 3.12 abi3 macOS 10.9+ x86-64 Details

Total release size: 373.0 kB

Release files / l1fit-0.1.0.tar.gz

Download URL l1fit-0.1.0.tar.gz
Size 35.0 kB
Tags Source
SHA-256 checksum
How to use checksums
1d2b6bdde5f03768ef6c9b2a397453cf8ae32add81756953a54ee6c86af1a3e8
BLAKE2b-256 checksum
How to use checksums
fbdcc29a7cc5cb930191da791278275f6e2e416913dbb22eab31530e2571786b
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.0-cp312-abi3-win_amd64.whl

Download URL l1fit-0.1.0-cp312-abi3-win_amd64.whl
Size 90.8 kB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
b45f4f1b629a182205f18c95c5900c2f1829cd99b5773b5afdba39f2474d22a2
BLAKE2b-256 checksum
How to use checksums
bce28f77aed0a762fc543bfce7666207ad02adb444eb3b193f0f7f05e8d46755
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.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl

Download URL l1fit-0.1.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Size 66.9 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
a073bd3f26e1ffacd4e978eae895a040ee71d31c1c5f0bc2cf838d0f79fbf41c
BLAKE2b-256 checksum
How to use checksums
d169c2e42073bdac0a6509c8997a95bd95375e80da621d3c2adb6791d45eb384
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.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl

Download URL l1fit-0.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl
Size 63.9 kB
Tags CPython 3.12 Linux glibc 2.26+ ARM64 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
8b27bf677d819debd8f105ffcaaed88525fad23c0d08bae3208382db8b912812
BLAKE2b-256 checksum
How to use checksums
3745758a504a7db4ec3091df808852ffa0c53b7f038d55c7b4f5ea3aebf259a7
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.0-cp312-abi3-macosx_11_0_arm64.whl

Download URL l1fit-0.1.0-cp312-abi3-macosx_11_0_arm64.whl
Size 57.4 kB
Tags CPython 3.12 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
feb17ad01151877c50426f1b4946d13a64ec09dff49f86365993f0863d8f8927
BLAKE2b-256 checksum
How to use checksums
4e80d58c9f386715c58d53b451f4f36458230b718fa87e4de3b350aa70d4ffa8
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.0-cp312-abi3-macosx_10_9_x86_64.whl

Download URL l1fit-0.1.0-cp312-abi3-macosx_10_9_x86_64.whl
Size 59.0 kB
Tags CPython 3.12 abi3 macOS 10.9+ x86-64
SHA-256 checksum
How to use checksums
d303a54ed532e1d089f44639331cd6fc8b97e3c9cc18802afd6bebdd3a599f9f
BLAKE2b-256 checksum
How to use checksums
a8c61c02887e37dcc3f47e2abf06e72eec0f7968d5879e874a322454478d6488
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

0.1.1

6 release files

This release

0.1.0 This release

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