fastprox 0.1.0rc1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastprox-0.1.0rc1/.github/workflows/bench.yml +52 -0
- fastprox-0.1.0rc1/.github/workflows/ci.yml +138 -0
- fastprox-0.1.0rc1/.github/workflows/fuzz.yml +36 -0
- fastprox-0.1.0rc1/.gitignore +13 -0
- fastprox-0.1.0rc1/.python-version +1 -0
- fastprox-0.1.0rc1/CHANGELOG.md +37 -0
- fastprox-0.1.0rc1/CLAUDE.md +34 -0
- fastprox-0.1.0rc1/CMakeLists.txt +161 -0
- fastprox-0.1.0rc1/LICENSE +21 -0
- fastprox-0.1.0rc1/PKG-INFO +100 -0
- fastprox-0.1.0rc1/README.md +73 -0
- fastprox-0.1.0rc1/ROADMAP.md +103 -0
- fastprox-0.1.0rc1/bench/bench_python.py +99 -0
- fastprox-0.1.0rc1/bench/compare_external.py +289 -0
- fastprox-0.1.0rc1/bindings/python/CMakeLists.txt +17 -0
- fastprox-0.1.0rc1/bindings/python/fastprox/__init__.py +433 -0
- fastprox-0.1.0rc1/bindings/python/src/module.cpp +224 -0
- fastprox-0.1.0rc1/cmake/fastprox.map +7 -0
- fastprox-0.1.0rc1/cmake/fastprox.pc.in +9 -0
- fastprox-0.1.0rc1/cmake/fastproxConfig.cmake.in +5 -0
- fastprox-0.1.0rc1/docs/api.md +204 -0
- fastprox-0.1.0rc1/docs/benchmarks.md +111 -0
- fastprox-0.1.0rc1/docs/spec.md +461 -0
- fastprox-0.1.0rc1/fuzz/CMakeLists.txt +20 -0
- fastprox-0.1.0rc1/fuzz/fuzz_ops.cpp +296 -0
- fastprox-0.1.0rc1/fuzz/standalone.cpp +79 -0
- fastprox-0.1.0rc1/include/fastprox.h +205 -0
- fastprox-0.1.0rc1/pyproject.toml +102 -0
- fastprox-0.1.0rc1/src/api/api.cpp +515 -0
- fastprox-0.1.0rc1/src/core/driver.hpp +320 -0
- fastprox-0.1.0rc1/src/core/layout.hpp +163 -0
- fastprox-0.1.0rc1/src/core/op.hpp +304 -0
- fastprox-0.1.0rc1/src/core/workspace.hpp +91 -0
- fastprox-0.1.0rc1/src/ops/group.cpp +146 -0
- fastprox-0.1.0rc1/src/ops/isotonic.cpp +124 -0
- fastprox-0.1.0rc1/src/ops/ops.hpp +41 -0
- fastprox-0.1.0rc1/src/ops/pointwise.cpp +235 -0
- fastprox-0.1.0rc1/src/ops/projections.cpp +556 -0
- fastprox-0.1.0rc1/src/ops/tv1d.cpp +525 -0
- fastprox-0.1.0rc1/tests/c/CMakeLists.txt +12 -0
- fastprox-0.1.0rc1/tests/c/test_core.cpp +532 -0
- fastprox-0.1.0rc1/tests/c/test_header.c +65 -0
- fastprox-0.1.0rc1/tests/fixtures/generate.py +84 -0
- fastprox-0.1.0rc1/tests/fixtures/reference.npz +0 -0
- fastprox-0.1.0rc1/tests/python/conftest.py +2 -0
- fastprox-0.1.0rc1/tests/python/fixture_cases.py +117 -0
- fastprox-0.1.0rc1/tests/python/reference.py +104 -0
- fastprox-0.1.0rc1/tests/python/test_fixtures.py +42 -0
- fastprox-0.1.0rc1/tests/python/test_operators.py +381 -0
- fastprox-0.1.0rc1/uv.lock +869 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: bench
|
|
2
|
+
|
|
3
|
+
# Benchmarks on GitHub's Linux runners: homogeneous cores (unlike a laptop
|
|
4
|
+
# with performance and efficiency cores), but shared VMs, so compare solvers
|
|
5
|
+
# within one run rather than across runs. Started by hand from the Actions tab.
|
|
6
|
+
on:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
full:
|
|
10
|
+
description: "All sizes up to 1e7 (slow) instead of 1e3 and 1e5"
|
|
11
|
+
type: boolean
|
|
12
|
+
default: false
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
bench:
|
|
16
|
+
name: bench ${{ matrix.os }}
|
|
17
|
+
runs-on: ${{ matrix.os }}
|
|
18
|
+
timeout-minutes: 90
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
os: [ubuntu-latest, ubuntu-24.04-arm]
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: astral-sh/setup-uv@v6
|
|
26
|
+
- name: Tools for the external codes (proxTV needs LAPACK, Condat's files GSL)
|
|
27
|
+
run: sudo apt-get update && sudo apt-get install -y liblapack-dev libgsl-dev
|
|
28
|
+
- run: uv sync --python 3.12
|
|
29
|
+
- name: Machine
|
|
30
|
+
run: |
|
|
31
|
+
{
|
|
32
|
+
lscpu | grep -E 'Model name|^CPU\(s\)|Thread|Core|Socket|MHz|L2|L3' || true
|
|
33
|
+
cc --version | head -1
|
|
34
|
+
c++ --version | head -1
|
|
35
|
+
} | tee machine.txt
|
|
36
|
+
{ echo "### ${{ matrix.os }}"; echo '```'; cat machine.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
|
|
37
|
+
- name: Overhead, throughput and parallel scaling
|
|
38
|
+
run: uv run python bench/bench_python.py | tee bench_python.txt
|
|
39
|
+
- name: Against proxTV and Condat's code
|
|
40
|
+
run: uv run python bench/compare_external.py ${{ inputs.full && ' ' || '--quick' }} | tee compare_external.txt
|
|
41
|
+
- name: Summary
|
|
42
|
+
if: always()
|
|
43
|
+
run: |
|
|
44
|
+
for f in bench_python.txt compare_external.txt; do
|
|
45
|
+
[ -f "$f" ] || continue
|
|
46
|
+
{ echo "#### $f"; echo '```'; cat "$f"; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
|
|
47
|
+
done
|
|
48
|
+
- uses: actions/upload-artifact@v4
|
|
49
|
+
if: always()
|
|
50
|
+
with:
|
|
51
|
+
name: bench-${{ matrix.os }}
|
|
52
|
+
path: "*.txt"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
workflow_dispatch: # also builds the wheels, without a tag
|
|
7
|
+
inputs:
|
|
8
|
+
publish:
|
|
9
|
+
description: Upload the wheels and sdist to TestPyPI
|
|
10
|
+
type: boolean
|
|
11
|
+
default: false
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
c:
|
|
15
|
+
name: C/C++ ${{ matrix.os }} ${{ matrix.cc }} openmp=${{ matrix.openmp }}
|
|
16
|
+
runs-on: ${{ matrix.os }}
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
include:
|
|
21
|
+
- { os: ubuntu-latest, cc: gcc, cxx: g++, openmp: ON }
|
|
22
|
+
- { os: ubuntu-latest, cc: clang, cxx: clang++, openmp: ON }
|
|
23
|
+
- { os: ubuntu-latest, cc: gcc, cxx: g++, openmp: OFF }
|
|
24
|
+
- { os: macos-latest, cc: clang, cxx: clang++, openmp: ON }
|
|
25
|
+
- { os: macos-15-intel, cc: clang, cxx: clang++, openmp: ON }
|
|
26
|
+
- { os: windows-latest, cc: cl, cxx: cl, openmp: ON }
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- if: runner.os == 'macOS'
|
|
30
|
+
run: brew install libomp
|
|
31
|
+
- name: Configure
|
|
32
|
+
env: { CC: "${{ matrix.cc }}", CXX: "${{ matrix.cxx }}" }
|
|
33
|
+
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DFASTPROX_OPENMP=${{ matrix.openmp }}
|
|
34
|
+
- run: cmake --build build --config Release
|
|
35
|
+
- run: ctest --test-dir build -C Release --output-on-failure --timeout 300
|
|
36
|
+
|
|
37
|
+
sanitizers:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DFASTPROX_SANITIZE=ON -DFASTPROX_OPENMP=OFF -DFASTPROX_FUZZ=ON
|
|
42
|
+
- run: cmake --build build
|
|
43
|
+
- run: ctest --test-dir build --output-on-failure --timeout 300
|
|
44
|
+
|
|
45
|
+
valgrind:
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- run: sudo apt-get update && sudo apt-get install -y valgrind
|
|
50
|
+
- run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DFASTPROX_OPENMP=OFF
|
|
51
|
+
- run: cmake --build build
|
|
52
|
+
- name: Memcheck
|
|
53
|
+
run: |
|
|
54
|
+
for t in build/tests/c/test_header build/tests/c/test_core; do
|
|
55
|
+
valgrind --error-exitcode=1 --leak-check=full --errors-for-leak-kinds=definite "$t"
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
python:
|
|
59
|
+
name: Python ${{ matrix.python }} ${{ matrix.os }}
|
|
60
|
+
runs-on: ${{ matrix.os }}
|
|
61
|
+
strategy:
|
|
62
|
+
fail-fast: false
|
|
63
|
+
matrix:
|
|
64
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
65
|
+
python: ["3.12"]
|
|
66
|
+
include:
|
|
67
|
+
- { os: ubuntu-latest, python: "3.14" } # the abi3 wheel on a newer CPython
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v4
|
|
70
|
+
- if: runner.os == 'macOS'
|
|
71
|
+
run: brew install libomp
|
|
72
|
+
- uses: astral-sh/setup-uv@v6
|
|
73
|
+
- run: uv sync --python ${{ matrix.python }}
|
|
74
|
+
- run: uv run pytest -q
|
|
75
|
+
|
|
76
|
+
wheels:
|
|
77
|
+
name: Wheels ${{ matrix.os }}
|
|
78
|
+
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
|
|
79
|
+
runs-on: ${{ matrix.os }}
|
|
80
|
+
strategy:
|
|
81
|
+
matrix:
|
|
82
|
+
# macOS runners are pinned: the wheel's minimum macOS is that of the
|
|
83
|
+
# runner's Homebrew libomp, which must match MACOSX_DEPLOYMENT_TARGET.
|
|
84
|
+
os: [ubuntu-latest, ubuntu-24.04-arm, macos-14, macos-15-intel, windows-latest]
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- uses: pypa/cibuildwheel@v3.1
|
|
88
|
+
- uses: actions/upload-artifact@v4
|
|
89
|
+
with:
|
|
90
|
+
name: wheels-${{ matrix.os }}
|
|
91
|
+
path: wheelhouse/*.whl
|
|
92
|
+
|
|
93
|
+
sdist:
|
|
94
|
+
name: Source distribution
|
|
95
|
+
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
steps:
|
|
98
|
+
- uses: actions/checkout@v4
|
|
99
|
+
- uses: astral-sh/setup-uv@v6
|
|
100
|
+
- run: uv build --sdist
|
|
101
|
+
- name: Tag matches the package version
|
|
102
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
103
|
+
run: |
|
|
104
|
+
version=$(tar xzf dist/*.tar.gz -O --wildcards '*/PKG-INFO' | sed -n 's/^Version: //p')
|
|
105
|
+
test "${GITHUB_REF_NAME}" = "v${version}" || {
|
|
106
|
+
echo "tag ${GITHUB_REF_NAME} does not match version ${version}"; exit 1; }
|
|
107
|
+
- uses: actions/upload-artifact@v4
|
|
108
|
+
with:
|
|
109
|
+
name: sdist
|
|
110
|
+
path: dist/*.tar.gz
|
|
111
|
+
|
|
112
|
+
# Trusted publishing (OIDC): no API tokens are stored in the repository.
|
|
113
|
+
# The publishers are registered on PyPI and TestPyPI for this workflow file
|
|
114
|
+
# and these environment names.
|
|
115
|
+
publish-testpypi:
|
|
116
|
+
name: Publish to TestPyPI
|
|
117
|
+
if: github.event_name == 'workflow_dispatch' && inputs.publish
|
|
118
|
+
needs: [c, sanitizers, valgrind, python, wheels, sdist]
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
environment: { name: testpypi, url: "https://test.pypi.org/p/fastprox" }
|
|
121
|
+
permissions: { id-token: write }
|
|
122
|
+
steps:
|
|
123
|
+
- uses: actions/download-artifact@v4
|
|
124
|
+
with: { path: dist, merge-multiple: true }
|
|
125
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
126
|
+
with: { repository-url: "https://test.pypi.org/legacy/" }
|
|
127
|
+
|
|
128
|
+
publish-pypi:
|
|
129
|
+
name: Publish to PyPI
|
|
130
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
131
|
+
needs: [c, sanitizers, valgrind, python, wheels, sdist]
|
|
132
|
+
runs-on: ubuntu-latest
|
|
133
|
+
environment: { name: pypi, url: "https://pypi.org/p/fastprox" }
|
|
134
|
+
permissions: { id-token: write }
|
|
135
|
+
steps:
|
|
136
|
+
- uses: actions/download-artifact@v4
|
|
137
|
+
with: { path: dist, merge-multiple: true }
|
|
138
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: fuzz
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "17 3 * * *" # nightly
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
libfuzzer:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/cache@v4
|
|
14
|
+
with:
|
|
15
|
+
path: corpus
|
|
16
|
+
key: fuzz-corpus-${{ github.run_id }}
|
|
17
|
+
restore-keys: fuzz-corpus-
|
|
18
|
+
- name: Build
|
|
19
|
+
env: { CC: clang, CXX: clang++ }
|
|
20
|
+
run: |
|
|
21
|
+
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DFASTPROX_FUZZ=ON \
|
|
22
|
+
-DFASTPROX_SANITIZE=ON -DFASTPROX_OPENMP=OFF -DBUILD_SHARED_LIBS=OFF
|
|
23
|
+
cmake --build build --target fuzz_ops
|
|
24
|
+
- name: Fuzz for 20 minutes
|
|
25
|
+
run: |
|
|
26
|
+
mkdir -p corpus
|
|
27
|
+
./build/fuzz/fuzz_ops corpus -max_total_time=1200 -timeout=10 -rss_limit_mb=4096 \
|
|
28
|
+
-max_len=8192 -print_final_stats=1
|
|
29
|
+
- if: failure()
|
|
30
|
+
uses: actions/upload-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: fuzz-crashes
|
|
33
|
+
path: |
|
|
34
|
+
crash-*
|
|
35
|
+
timeout-*
|
|
36
|
+
leak-*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to fastprox. Planned work is in [ROADMAP.md](ROADMAP.md).
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased] — 0.1.0rc1
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Plain-C API (`include/fastprox.h`) over a C++17 core: opaque operator and workspace handles, `fp_prox`, `fp_prox_conj`, `fp_jvp`, `fp_vjp`, error codes with every exception caught at the boundary.
|
|
11
|
+
- Operators: soft thresholding (weighted), elastic net, Huber, group ℓ2 and ℓ2 norm, box, nonnegative orthant, ℓ2 ball, simplex, weighted ℓ1 ball, ℓ∞ norm, isotonic regression, and 1D TV (Condat's direct algorithm and taut string, edge weights).
|
|
12
|
+
- JVP and VJP in the input, the step and the operator parameters for every operator.
|
|
13
|
+
- Batching over any axis of strided n-d arrays, OpenMP parallelism with bit-identical results for any thread count, and allocation-free calls with a workspace.
|
|
14
|
+
- Python package built with nanobind (abi3, CPython ≥ 3.12): one-shot functions and operator classes with `prox`, `prox_conj`, `jvp` and `vjp`.
|
|
15
|
+
- C/C++ and Python test suites, a benchmark script, and a CI workflow.
|
|
16
|
+
- Release pipeline: sdist job, trusted publishing to TestPyPI (manual dispatch with `publish`) and to PyPI (tags `v*`, checked against the package version); PyPI metadata and a README for the project page; pre-release tags through `FASTPROX_VERSION_SUFFIX` in `CMakeLists.txt`; Python tests also run on CPython 3.14.
|
|
17
|
+
- Fuzz harness for the whole API (libFuzzer target and standalone driver) with a nightly workflow; CVXPY + Clarabel reference fixtures; benchmarks against proxTV and Condat's code (`bench/compare_external.py`, `docs/benchmarks.md`); API reference (`docs/api.md`); Valgrind CI job.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- 1D TV: the Condat path falls back to the taut string after 3n + 1024 rescanned steps (was 16n + 1024 steps in total), which makes adversarial inputs such as ramps about 3.5× cheaper without affecting typical signals.
|
|
22
|
+
- 1D TV: the bound updates of Condat's algorithm are branch-free selects with a table of reciprocals, 25–35% faster on noisy signals (same arithmetic, so the same results).
|
|
23
|
+
- 1D TV: weighted problems now use Condat's direct algorithm, generalised to per-edge weights (radii precomputed per piece), with the same budgeted fallback to the taut string; `FP_TV1D_CONDAT` accepts weights. Weighted rows went from 1.4–2.8× slower than proxTV's weighted taut string to faster in most settings.
|
|
24
|
+
- 1D TV: radii that can never be active (above the dual bound j · (max x − min x)) are clamped, and when no edge can be active the result is the constant mean. Extreme weights or steps no longer cost accuracy: the residual stays within 10 n ε ‖x‖∞.
|
|
25
|
+
- 1D TV: the taut string stores its two chains as segments (length, rise, cached slope) in coordinates local to the current knot, an idea taken from proxTV's classic taut string. It is 17–26% faster and needs no compensated sums: the cumulative sum of the signal is never formed.
|
|
26
|
+
- Unweighted simplex and ℓ1-ball projections keep candidate values instead of indices (about 30% faster). The simplex also tests finiteness inside its pivot pass instead of a separate pass (a further 19%), and the ℓ1 ball and ℓ∞ prox reuse their ℓ1 norm as the test.
|
|
27
|
+
- 1D TV: bound updates are branch-free only in the first 8 samples of a segment; weighted rows read their radii directly when none can need clamping; operator construction makes one pass over the weights.
|
|
28
|
+
- Workspace scratch is no longer zero-filled, and the finiteness check is a branch-free pass: one-shot calls on large rows are noticeably faster.
|
|
29
|
+
- The version is defined once, in `CMakeLists.txt`; the Python package reads it from there.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Weighted ℓ1-ball projection returned infinities for tiny weights (around 1e-279) because sums of squared weights underflowed (found by fuzzing). Weights are now normalised by their maximum, and ratios below 1e-100 are rejected.
|
|
34
|
+
- `fp_prox_conj` with σ = 0 on arrays with negative strides computed byte offsets in unsigned arithmetic (undefined behaviour, found by fuzzing).
|
|
35
|
+
- Static Windows builds marked API symbols `dllexport`.
|
|
36
|
+
- The taut string kept cumulative sums in plain doubles and missed the 10 n ε ‖x‖∞ accuracy bound on long rows; its chains now use local coordinates.
|
|
37
|
+
- A test assumed weighted isotonic regression is firmly nonexpansive in the Euclidean norm; it is so in the weighted norm, which the test now uses.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# fastprox
|
|
2
|
+
|
|
3
|
+
Exact proximal operators with JVP/VJP. C++17 core behind the plain-C header
|
|
4
|
+
`include/fastprox.h`; Python bindings via nanobind. The design spec is
|
|
5
|
+
`docs/spec.md` — read it before changing the API or adding operators.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
- C library and tests: `cmake -S . -B build/c -G Ninja && cmake --build build/c && ./build/c/tests/c/test_core && ./build/c/tests/c/test_header`
|
|
10
|
+
- Sanitizers: configure a second tree with `-DFASTPROX_SANITIZE=ON -DFASTPROX_OPENMP=OFF -DCMAKE_BUILD_TYPE=Debug`
|
|
11
|
+
- Python (always through uv): `uv sync`, `uv run pytest -q`. uv rebuilds the extension when C++ sources change (`tool.uv.cache-keys`).
|
|
12
|
+
- Benchmarks: `uv run python bench/bench_python.py`
|
|
13
|
+
- Releasing: set the version in `project()` and `FASTPROX_VERSION_SUFFIX` (`CMakeLists.txt`) and the macros in `fastprox.h`; dry run with `gh workflow run ci.yml -f publish=true` (TestPyPI); then push the tag `v<version>`, which publishes to PyPI.
|
|
14
|
+
|
|
15
|
+
## Layout
|
|
16
|
+
|
|
17
|
+
- `src/core/op.hpp` — `fp_op` base class; `ElemOp<D>` (elementwise, needs `prox1`/`deriv1`) and `RowOp<D>` (row kernels `prox`/`jvp`/`vjp`) CRTP helpers.
|
|
18
|
+
- `src/core/driver.hpp` — batching over strided n-d arrays, OpenMP, saved structure; `src/core/layout.hpp` — row and elementwise iteration plans.
|
|
19
|
+
- `src/ops/*.cpp` — one file per operator family, each with a `make_*` factory declared in `src/ops/ops.hpp`.
|
|
20
|
+
- `src/api/api.cpp` — the only `extern "C"` code: validation, dtype dispatch, try/catch.
|
|
21
|
+
- `bindings/python/src/module.cpp` calls only the C API; `bindings/python/fastprox/__init__.py` is the user-facing package.
|
|
22
|
+
|
|
23
|
+
## Tracking
|
|
24
|
+
|
|
25
|
+
- `ROADMAP.md` lists planned work per release with status boxes; tick items as they land and add new ones as they come up.
|
|
26
|
+
- `CHANGELOG.md` records finished changes under `[Unreleased]`, in the same change that makes them.
|
|
27
|
+
- Any decision that departs from `docs/spec.md` goes in the decisions table at the end of `ROADMAP.md`, with its date.
|
|
28
|
+
|
|
29
|
+
## Rules
|
|
30
|
+
|
|
31
|
+
- Clean room: never read GPL, CeCILL or unlicensed code (Condat's C files, Kolmogorov's maxflow/TVTree, Chambolle's graphTV, PyMaxflow, gco). Implement from papers and cite them in the kernel's header comment. proxTV (BSD) may be consulted with attribution.
|
|
32
|
+
- No `-ffast-math`. Results must be bit-identical for any thread count: rows are independent, and cross-row sums (vjp parameter gradients) use fixed blocks from `reduction_blocks`, never thread-dependent partitions.
|
|
33
|
+
- No allocation inside `fp_prox`/`fp_jvp`/`fp_vjp` when a workspace is given; size everything in `build_workspace` (tested by an allocation counter in `tests/c/test_core.cpp`).
|
|
34
|
+
- Adding an operator: kernel + derivatives in `src/ops/`, factory in `ops.hpp`, constructor in `fastprox.h` and `api.cpp`, nanobind wrapper, Python class with `_layout`, and an entry in `CASES` in `tests/python/test_operators.py` (which runs reference, finite-difference, adjointness, nonexpansiveness, threading and stride tests on it) plus `derivative_cases` in `tests/c/test_core.cpp`.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.20)
|
|
2
|
+
project(fastprox VERSION 0.1.0 LANGUAGES C CXX)
|
|
3
|
+
# PEP 440 pre-release tag (a1, rc1, ...) appended to the version; empty for a
|
|
4
|
+
# final release. pyproject.toml reads both lines.
|
|
5
|
+
set(FASTPROX_VERSION_SUFFIX "rc1")
|
|
6
|
+
set(FASTPROX_VERSION_FULL "${PROJECT_VERSION}${FASTPROX_VERSION_SUFFIX}")
|
|
7
|
+
|
|
8
|
+
option(FASTPROX_OPENMP "Parallelise over rows with OpenMP" ON)
|
|
9
|
+
option(FASTPROX_BUILD_TESTS "Build the C and C++ tests" ${PROJECT_IS_TOP_LEVEL})
|
|
10
|
+
option(FASTPROX_PYTHON "Build the nanobind Python extension" OFF)
|
|
11
|
+
option(FASTPROX_SANITIZE "Build with AddressSanitizer and UBSan" OFF)
|
|
12
|
+
option(FASTPROX_FUZZ "Build the fuzzing harnesses" OFF)
|
|
13
|
+
option(BUILD_SHARED_LIBS "Build libfastprox as a shared library" ON)
|
|
14
|
+
|
|
15
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
16
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
17
|
+
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
18
|
+
set(CMAKE_C_STANDARD 99)
|
|
19
|
+
set(CMAKE_C_STANDARD_REQUIRED ON)
|
|
20
|
+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
|
21
|
+
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
|
22
|
+
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
|
23
|
+
endif()
|
|
24
|
+
|
|
25
|
+
include(GNUInstallDirs)
|
|
26
|
+
|
|
27
|
+
# Windows finds DLLs next to the executable: keep the library, tests and fuzz
|
|
28
|
+
# drivers in one directory, or every test hangs on a "DLL not found" dialog.
|
|
29
|
+
if(WIN32 AND NOT CMAKE_RUNTIME_OUTPUT_DIRECTORY)
|
|
30
|
+
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
|
31
|
+
endif()
|
|
32
|
+
|
|
33
|
+
# project() above is the single source of the version (pyproject.toml reads it
|
|
34
|
+
# too); the public header's macros must agree with it.
|
|
35
|
+
file(STRINGS include/fastprox.h _fp_version_lines REGEX "#define FP_VERSION_(MAJOR|MINOR|PATCH) ")
|
|
36
|
+
string(REGEX REPLACE ".*MAJOR ([0-9]+).*MINOR ([0-9]+).*PATCH ([0-9]+).*" "\\1.\\2.\\3"
|
|
37
|
+
_fp_header_version "${_fp_version_lines}")
|
|
38
|
+
if(NOT _fp_header_version STREQUAL PROJECT_VERSION)
|
|
39
|
+
message(FATAL_ERROR "fastprox.h declares version ${_fp_header_version}, "
|
|
40
|
+
"CMakeLists.txt ${PROJECT_VERSION}")
|
|
41
|
+
endif()
|
|
42
|
+
|
|
43
|
+
# The Python extension links the core statically.
|
|
44
|
+
if(FASTPROX_PYTHON)
|
|
45
|
+
set(BUILD_SHARED_LIBS OFF)
|
|
46
|
+
endif()
|
|
47
|
+
|
|
48
|
+
add_library(fastprox
|
|
49
|
+
src/api/api.cpp
|
|
50
|
+
src/ops/pointwise.cpp
|
|
51
|
+
src/ops/group.cpp
|
|
52
|
+
src/ops/projections.cpp
|
|
53
|
+
src/ops/isotonic.cpp
|
|
54
|
+
src/ops/tv1d.cpp
|
|
55
|
+
)
|
|
56
|
+
add_library(fastprox::fastprox ALIAS fastprox)
|
|
57
|
+
|
|
58
|
+
target_include_directories(fastprox
|
|
59
|
+
PUBLIC
|
|
60
|
+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
|
61
|
+
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
|
62
|
+
PRIVATE
|
|
63
|
+
${CMAKE_CURRENT_SOURCE_DIR}/src
|
|
64
|
+
)
|
|
65
|
+
target_compile_definitions(fastprox
|
|
66
|
+
PRIVATE FASTPROX_BUILDING FASTPROX_VERSION_STRING="${FASTPROX_VERSION_FULL}")
|
|
67
|
+
if(NOT BUILD_SHARED_LIBS)
|
|
68
|
+
target_compile_definitions(fastprox PUBLIC FASTPROX_STATIC)
|
|
69
|
+
endif()
|
|
70
|
+
set_target_properties(fastprox PROPERTIES
|
|
71
|
+
CXX_VISIBILITY_PRESET hidden
|
|
72
|
+
VISIBILITY_INLINES_HIDDEN ON
|
|
73
|
+
POSITION_INDEPENDENT_CODE ON
|
|
74
|
+
VERSION ${PROJECT_VERSION}
|
|
75
|
+
SOVERSION ${PROJECT_VERSION_MAJOR}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# No -ffast-math anywhere: the direct algorithms compare running bounds and
|
|
79
|
+
# determinism depends on the order of floating-point operations.
|
|
80
|
+
if(MSVC)
|
|
81
|
+
target_compile_options(fastprox PRIVATE /W4 /fp:precise)
|
|
82
|
+
else()
|
|
83
|
+
target_compile_options(fastprox PRIVATE -Wall -Wextra -Wno-unknown-pragmas -fno-fast-math)
|
|
84
|
+
endif()
|
|
85
|
+
|
|
86
|
+
if(FASTPROX_OPENMP)
|
|
87
|
+
if(APPLE AND NOT OpenMP_ROOT)
|
|
88
|
+
# Apple Clang ships without libomp; use Homebrew's when present.
|
|
89
|
+
foreach(_prefix /opt/homebrew/opt/libomp /usr/local/opt/libomp)
|
|
90
|
+
if(EXISTS "${_prefix}/include/omp.h")
|
|
91
|
+
set(OpenMP_ROOT "${_prefix}")
|
|
92
|
+
break()
|
|
93
|
+
endif()
|
|
94
|
+
endforeach()
|
|
95
|
+
endif()
|
|
96
|
+
find_package(OpenMP COMPONENTS CXX)
|
|
97
|
+
if(OpenMP_CXX_FOUND)
|
|
98
|
+
target_link_libraries(fastprox PRIVATE OpenMP::OpenMP_CXX)
|
|
99
|
+
else()
|
|
100
|
+
message(WARNING "OpenMP not found: fastprox will run serially")
|
|
101
|
+
endif()
|
|
102
|
+
endif()
|
|
103
|
+
|
|
104
|
+
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
|
105
|
+
# Keep the host libstdc++ out of the picture (MATLAB, old distributions).
|
|
106
|
+
target_link_options(fastprox PRIVATE -static-libstdc++ -static-libgcc)
|
|
107
|
+
endif()
|
|
108
|
+
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND BUILD_SHARED_LIBS)
|
|
109
|
+
target_link_options(fastprox PRIVATE
|
|
110
|
+
"-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/cmake/fastprox.map")
|
|
111
|
+
endif()
|
|
112
|
+
|
|
113
|
+
if(FASTPROX_SANITIZE AND NOT MSVC)
|
|
114
|
+
add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer)
|
|
115
|
+
add_link_options(-fsanitize=address,undefined)
|
|
116
|
+
target_compile_options(fastprox PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
|
|
117
|
+
target_link_options(fastprox PUBLIC -fsanitize=address,undefined)
|
|
118
|
+
endif()
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------- install
|
|
121
|
+
if(NOT FASTPROX_PYTHON)
|
|
122
|
+
include(CMakePackageConfigHelpers)
|
|
123
|
+
install(TARGETS fastprox EXPORT fastproxTargets
|
|
124
|
+
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
|
125
|
+
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
|
126
|
+
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
|
127
|
+
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
|
128
|
+
install(FILES include/fastprox.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
|
129
|
+
install(EXPORT fastproxTargets NAMESPACE fastprox::
|
|
130
|
+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/fastprox)
|
|
131
|
+
configure_package_config_file(cmake/fastproxConfig.cmake.in
|
|
132
|
+
${CMAKE_CURRENT_BINARY_DIR}/fastproxConfig.cmake
|
|
133
|
+
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/fastprox)
|
|
134
|
+
write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/fastproxConfigVersion.cmake
|
|
135
|
+
COMPATIBILITY SameMajorVersion)
|
|
136
|
+
install(FILES
|
|
137
|
+
${CMAKE_CURRENT_BINARY_DIR}/fastproxConfig.cmake
|
|
138
|
+
${CMAKE_CURRENT_BINARY_DIR}/fastproxConfigVersion.cmake
|
|
139
|
+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/fastprox)
|
|
140
|
+
configure_file(cmake/fastprox.pc.in ${CMAKE_CURRENT_BINARY_DIR}/fastprox.pc @ONLY)
|
|
141
|
+
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/fastprox.pc
|
|
142
|
+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
|
143
|
+
endif()
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------- tests
|
|
146
|
+
if(FASTPROX_BUILD_TESTS)
|
|
147
|
+
enable_testing()
|
|
148
|
+
add_subdirectory(tests/c)
|
|
149
|
+
endif()
|
|
150
|
+
|
|
151
|
+
if(FASTPROX_FUZZ)
|
|
152
|
+
if(NOT FASTPROX_BUILD_TESTS)
|
|
153
|
+
enable_testing()
|
|
154
|
+
endif()
|
|
155
|
+
add_subdirectory(fuzz)
|
|
156
|
+
endif()
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------- python
|
|
159
|
+
if(FASTPROX_PYTHON)
|
|
160
|
+
add_subdirectory(bindings/python)
|
|
161
|
+
endif()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Villacís
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastprox
|
|
3
|
+
Version: 0.1.0rc1
|
|
4
|
+
Summary: Exact proximal operators and projections with derivatives
|
|
5
|
+
Keywords: proximal operator,projection,total variation,optimization,bilevel,autodiff
|
|
6
|
+
Author-Email: David Villacís <david@villacis.net>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: C++
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
20
|
+
Project-URL: Homepage, https://github.com/dvillacis/fastprox
|
|
21
|
+
Project-URL: Documentation, https://github.com/dvillacis/fastprox/blob/master/docs/api.md
|
|
22
|
+
Project-URL: Changelog, https://github.com/dvillacis/fastprox/blob/master/CHANGELOG.md
|
|
23
|
+
Project-URL: Issues, https://github.com/dvillacis/fastprox/issues
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Requires-Dist: numpy>=1.26
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# fastprox
|
|
29
|
+
|
|
30
|
+
Exact proximal operators and projections with derivatives. A C++17 core
|
|
31
|
+
behind a plain-C API, with Python bindings.
|
|
32
|
+
|
|
33
|
+
- **Exact.** Direct algorithms (Condat's 1D TV and simplex projection, the
|
|
34
|
+
taut string, pool-adjacent-violators) instead of iterative solvers.
|
|
35
|
+
- **Differentiable.** Jacobian-vector and vector-Jacobian products in the
|
|
36
|
+
input, the step and the operator's parameters, for bilevel learning and
|
|
37
|
+
implicit differentiation.
|
|
38
|
+
- **Batched.** Applied along any axis of a float32 or float64 array, with rows
|
|
39
|
+
processed in parallel by OpenMP and results bit-identical for any number of
|
|
40
|
+
threads.
|
|
41
|
+
- **Permissive.** MIT licensed; every kernel is written from the papers.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
pip install --pre fastprox # 0.1.0rc1, a release candidate
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Wheels are abi3 for CPython 3.12 and later on Linux (x86_64, aarch64), macOS
|
|
50
|
+
(arm64 from 14.0, x86_64 from 15.0) and Windows (amd64), with the OpenMP
|
|
51
|
+
runtime bundled.
|
|
52
|
+
|
|
53
|
+
## Use
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import numpy as np
|
|
57
|
+
import fastprox as fp
|
|
58
|
+
|
|
59
|
+
x = np.random.default_rng(0).normal(size=(64, 1000))
|
|
60
|
+
|
|
61
|
+
z = fp.tv1d(x, gamma=0.5) # 1D total variation along the last axis
|
|
62
|
+
p = fp.project_simplex(x, radius=1.0) # each row onto the simplex
|
|
63
|
+
s = fp.soft_threshold(x, lam=0.1)
|
|
64
|
+
|
|
65
|
+
# Operators with derivatives
|
|
66
|
+
op = fp.TV1D(weights=0.8)
|
|
67
|
+
z, ctx = op.prox(x, 0.5, keep=True)
|
|
68
|
+
dz = op.jvp(ctx, dx=np.ones_like(x)) # Jacobian-vector product
|
|
69
|
+
gx, grads = op.vjp(ctx, g=x) # vector-Jacobian product
|
|
70
|
+
print(grads.gamma, grads.weights) # gradients in the step and the weights
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Operators (0.1)
|
|
74
|
+
|
|
75
|
+
Soft thresholding (weighted), elastic net, Huber, group ℓ2, box,
|
|
76
|
+
nonnegative orthant, ℓ2 ball, simplex, weighted ℓ1 ball, ℓ∞ norm, isotonic
|
|
77
|
+
regression, and 1D total variation with per-edge weights. Each one has a
|
|
78
|
+
conjugate prox through the Moreau identity.
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
- [API reference](https://github.com/dvillacis/fastprox/blob/master/docs/api.md) (C and Python)
|
|
83
|
+
- [Benchmarks](https://github.com/dvillacis/fastprox/blob/master/docs/benchmarks.md) against proxTV and Condat's C code
|
|
84
|
+
- [Design specification](https://github.com/dvillacis/fastprox/blob/master/docs/spec.md)
|
|
85
|
+
- [Roadmap](https://github.com/dvillacis/fastprox/blob/master/ROADMAP.md) and [changelog](https://github.com/dvillacis/fastprox/blob/master/CHANGELOG.md)
|
|
86
|
+
|
|
87
|
+
## C library
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
|
91
|
+
cmake --build build && cmake --install build --prefix /usr/local
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Then `find_package(fastprox)` and link `fastprox::fastprox`, or use
|
|
95
|
+
`pkg-config --cflags --libs fastprox`. The header is
|
|
96
|
+
[`include/fastprox.h`](https://github.com/dvillacis/fastprox/blob/master/include/fastprox.h).
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|