fastprox 0.1.0__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.0/.github/workflows/bench.yml +52 -0
- fastprox-0.1.0/.github/workflows/ci.yml +151 -0
- fastprox-0.1.0/.github/workflows/fuzz.yml +36 -0
- fastprox-0.1.0/.github/workflows/profile.yml +99 -0
- fastprox-0.1.0/.github/workflows/variants.yml +89 -0
- fastprox-0.1.0/.gitignore +13 -0
- fastprox-0.1.0/.python-version +1 -0
- fastprox-0.1.0/CHANGELOG.md +84 -0
- fastprox-0.1.0/CLAUDE.md +34 -0
- fastprox-0.1.0/CMakeLists.txt +154 -0
- fastprox-0.1.0/LICENSE +21 -0
- fastprox-0.1.0/PKG-INFO +101 -0
- fastprox-0.1.0/README.md +74 -0
- fastprox-0.1.0/ROADMAP.md +118 -0
- fastprox-0.1.0/bench/asm_summary.py +65 -0
- fastprox-0.1.0/bench/bench_python.py +99 -0
- fastprox-0.1.0/bench/compare_external.py +292 -0
- fastprox-0.1.0/bench/profile_kernels.cpp +292 -0
- fastprox-0.1.0/bench/tv_variants.cpp +171 -0
- fastprox-0.1.0/bindings/python/CMakeLists.txt +17 -0
- fastprox-0.1.0/bindings/python/fastprox/__init__.py +435 -0
- fastprox-0.1.0/bindings/python/src/module.cpp +224 -0
- fastprox-0.1.0/cmake/fastprox.map +7 -0
- fastprox-0.1.0/cmake/fastprox.pc.in +10 -0
- fastprox-0.1.0/cmake/fastproxConfig.cmake.in +9 -0
- fastprox-0.1.0/docs/api.md +256 -0
- fastprox-0.1.0/docs/benchmarks.md +442 -0
- fastprox-0.1.0/docs/spec.md +461 -0
- fastprox-0.1.0/examples/README.md +20 -0
- fastprox-0.1.0/examples/basic_usage.py +73 -0
- fastprox-0.1.0/examples/bilevel_learning.py +85 -0
- fastprox-0.1.0/examples/derivatives.py +68 -0
- fastprox-0.1.0/examples/proximal_gradient.py +77 -0
- fastprox-0.1.0/examples/tv_denoising.py +63 -0
- fastprox-0.1.0/fuzz/CMakeLists.txt +20 -0
- fastprox-0.1.0/fuzz/fuzz_ops.cpp +296 -0
- fastprox-0.1.0/fuzz/standalone.cpp +79 -0
- fastprox-0.1.0/include/fastprox.h +208 -0
- fastprox-0.1.0/pyproject.toml +96 -0
- fastprox-0.1.0/src/api/api.cpp +509 -0
- fastprox-0.1.0/src/core/driver.hpp +299 -0
- fastprox-0.1.0/src/core/layout.hpp +163 -0
- fastprox-0.1.0/src/core/op.hpp +362 -0
- fastprox-0.1.0/src/core/parallel.cpp +244 -0
- fastprox-0.1.0/src/core/parallel.hpp +43 -0
- fastprox-0.1.0/src/core/workspace.hpp +91 -0
- fastprox-0.1.0/src/ops/group.cpp +146 -0
- fastprox-0.1.0/src/ops/isotonic.cpp +124 -0
- fastprox-0.1.0/src/ops/ops.hpp +41 -0
- fastprox-0.1.0/src/ops/pointwise.cpp +235 -0
- fastprox-0.1.0/src/ops/projections.cpp +608 -0
- fastprox-0.1.0/src/ops/tv1d.cpp +576 -0
- fastprox-0.1.0/tests/c/CMakeLists.txt +12 -0
- fastprox-0.1.0/tests/c/test_core.cpp +595 -0
- fastprox-0.1.0/tests/c/test_header.c +65 -0
- fastprox-0.1.0/tests/fixtures/generate.py +84 -0
- fastprox-0.1.0/tests/fixtures/reference.npz +0 -0
- fastprox-0.1.0/tests/python/conftest.py +2 -0
- fastprox-0.1.0/tests/python/fixture_cases.py +117 -0
- fastprox-0.1.0/tests/python/reference.py +104 -0
- fastprox-0.1.0/tests/python/test_examples.py +29 -0
- fastprox-0.1.0/tests/python/test_fixtures.py +42 -0
- fastprox-0.1.0/tests/python/test_operators.py +422 -0
- fastprox-0.1.0/tests/valgrind/fork.supp +12 -0
- fastprox-0.1.0/uv.lock +1291 -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,151 @@
|
|
|
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 }} threads=${{ matrix.threads }}
|
|
16
|
+
runs-on: ${{ matrix.os }}
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
include:
|
|
21
|
+
- { os: ubuntu-latest, cc: gcc, cxx: g++, threads: ON }
|
|
22
|
+
- { os: ubuntu-latest, cc: clang, cxx: clang++, threads: ON }
|
|
23
|
+
- { os: ubuntu-latest, cc: gcc, cxx: g++, threads: OFF }
|
|
24
|
+
- { os: macos-latest, cc: clang, cxx: clang++, threads: ON }
|
|
25
|
+
- { os: macos-15-intel, cc: clang, cxx: clang++, threads: ON }
|
|
26
|
+
- { os: windows-latest, cc: cl, cxx: cl, threads: ON }
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- name: Configure
|
|
30
|
+
env: { CC: "${{ matrix.cc }}", CXX: "${{ matrix.cxx }}" }
|
|
31
|
+
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DFASTPROX_THREADS=${{ matrix.threads }}
|
|
32
|
+
- run: cmake --build build --config Release
|
|
33
|
+
- run: ctest --test-dir build -C Release --output-on-failure --timeout 300
|
|
34
|
+
|
|
35
|
+
sanitizers:
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
- run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DFASTPROX_SANITIZE=ON -DFASTPROX_FUZZ=ON
|
|
40
|
+
- run: cmake --build build
|
|
41
|
+
- run: ctest --test-dir build --output-on-failure --timeout 300
|
|
42
|
+
|
|
43
|
+
tsan:
|
|
44
|
+
name: ThreadSanitizer
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v4
|
|
48
|
+
# TSan in clang 18 rejects the runner kernel's high ASLR entropy.
|
|
49
|
+
- run: sudo sysctl vm.mmap_rnd_bits=28
|
|
50
|
+
- name: Configure
|
|
51
|
+
env: { CC: clang, CXX: clang++ }
|
|
52
|
+
run: >
|
|
53
|
+
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
|
54
|
+
-DCMAKE_C_FLAGS=-fsanitize=thread -DCMAKE_CXX_FLAGS=-fsanitize=thread
|
|
55
|
+
-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=thread -DCMAKE_SHARED_LINKER_FLAGS=-fsanitize=thread
|
|
56
|
+
- run: cmake --build build
|
|
57
|
+
- run: ctest --test-dir build --output-on-failure --timeout 300
|
|
58
|
+
env: { TSAN_OPTIONS: "halt_on_error=1" }
|
|
59
|
+
|
|
60
|
+
valgrind:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
- run: sudo apt-get update && sudo apt-get install -y valgrind
|
|
65
|
+
- run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
|
|
66
|
+
- run: cmake --build build
|
|
67
|
+
- name: Memcheck
|
|
68
|
+
run: |
|
|
69
|
+
for t in build/tests/c/test_header build/tests/c/test_core; do
|
|
70
|
+
valgrind --error-exitcode=1 --leak-check=full --errors-for-leak-kinds=definite \
|
|
71
|
+
--suppressions=tests/valgrind/fork.supp "$t"
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
python:
|
|
75
|
+
name: Python ${{ matrix.python }} ${{ matrix.os }}
|
|
76
|
+
runs-on: ${{ matrix.os }}
|
|
77
|
+
strategy:
|
|
78
|
+
fail-fast: false
|
|
79
|
+
matrix:
|
|
80
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
81
|
+
python: ["3.12"]
|
|
82
|
+
include:
|
|
83
|
+
- { os: ubuntu-latest, python: "3.14" } # the abi3 wheel on a newer CPython
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@v4
|
|
86
|
+
- uses: astral-sh/setup-uv@v6
|
|
87
|
+
- run: uv sync --python ${{ matrix.python }}
|
|
88
|
+
- run: uv run pytest -q
|
|
89
|
+
|
|
90
|
+
wheels:
|
|
91
|
+
name: Wheels ${{ matrix.os }}
|
|
92
|
+
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
|
|
93
|
+
runs-on: ${{ matrix.os }}
|
|
94
|
+
strategy:
|
|
95
|
+
matrix:
|
|
96
|
+
# macos-15-intel builds the x86_64 wheel natively, so its tests run.
|
|
97
|
+
os: [ubuntu-latest, ubuntu-24.04-arm, macos-14, macos-15-intel, windows-latest]
|
|
98
|
+
steps:
|
|
99
|
+
- uses: actions/checkout@v4
|
|
100
|
+
- uses: pypa/cibuildwheel@v3.1
|
|
101
|
+
- uses: actions/upload-artifact@v4
|
|
102
|
+
with:
|
|
103
|
+
name: wheels-${{ matrix.os }}
|
|
104
|
+
path: wheelhouse/*.whl
|
|
105
|
+
|
|
106
|
+
sdist:
|
|
107
|
+
name: Source distribution
|
|
108
|
+
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
|
|
109
|
+
runs-on: ubuntu-latest
|
|
110
|
+
steps:
|
|
111
|
+
- uses: actions/checkout@v4
|
|
112
|
+
- uses: astral-sh/setup-uv@v6
|
|
113
|
+
- run: uv build --sdist
|
|
114
|
+
- name: Tag matches the package version
|
|
115
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
116
|
+
run: |
|
|
117
|
+
version=$(tar xzf dist/*.tar.gz -O --wildcards '*/PKG-INFO' | sed -n 's/^Version: //p')
|
|
118
|
+
test "${GITHUB_REF_NAME}" = "v${version}" || {
|
|
119
|
+
echo "tag ${GITHUB_REF_NAME} does not match version ${version}"; exit 1; }
|
|
120
|
+
- uses: actions/upload-artifact@v4
|
|
121
|
+
with:
|
|
122
|
+
name: sdist
|
|
123
|
+
path: dist/*.tar.gz
|
|
124
|
+
|
|
125
|
+
# Trusted publishing (OIDC): no API tokens are stored in the repository.
|
|
126
|
+
# The publishers are registered on PyPI and TestPyPI for this workflow file
|
|
127
|
+
# and these environment names.
|
|
128
|
+
publish-testpypi:
|
|
129
|
+
name: Publish to TestPyPI
|
|
130
|
+
if: github.event_name == 'workflow_dispatch' && inputs.publish
|
|
131
|
+
needs: [c, sanitizers, tsan, valgrind, python, wheels, sdist]
|
|
132
|
+
runs-on: ubuntu-latest
|
|
133
|
+
environment: { name: testpypi, url: "https://test.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
|
|
139
|
+
with: { repository-url: "https://test.pypi.org/legacy/" }
|
|
140
|
+
|
|
141
|
+
publish-pypi:
|
|
142
|
+
name: Publish to PyPI
|
|
143
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
144
|
+
needs: [c, sanitizers, tsan, valgrind, python, wheels, sdist]
|
|
145
|
+
runs-on: ubuntu-latest
|
|
146
|
+
environment: { name: pypi, url: "https://pypi.org/p/fastprox" }
|
|
147
|
+
permissions: { id-token: write }
|
|
148
|
+
steps:
|
|
149
|
+
- uses: actions/download-artifact@v4
|
|
150
|
+
with: { path: dist, merge-multiple: true }
|
|
151
|
+
- 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_THREADS=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,99 @@
|
|
|
1
|
+
name: profile
|
|
2
|
+
|
|
3
|
+
# Single-thread profile of 1D TV on GitHub's x86_64 runner, fastprox against
|
|
4
|
+
# proxTV, with hardware counters where the VM exposes them and GCC's assembly
|
|
5
|
+
# of the kernels. `ubuntu-latest` lands on an Intel or an AMD CPU at random,
|
|
6
|
+
# so this starts several identical jobs and labels each artifact with the CPU
|
|
7
|
+
# it got. Started by hand from the Actions tab.
|
|
8
|
+
on:
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
inputs:
|
|
11
|
+
jobs:
|
|
12
|
+
description: "Identical jobs to start (each lands on a random CPU), 1 to 12"
|
|
13
|
+
type: string
|
|
14
|
+
default: "6"
|
|
15
|
+
n:
|
|
16
|
+
description: "Samples per row"
|
|
17
|
+
type: string
|
|
18
|
+
default: "100000"
|
|
19
|
+
reps:
|
|
20
|
+
description: "Repetitions per setting (fresh input each)"
|
|
21
|
+
type: string
|
|
22
|
+
default: "31"
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
slots:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
outputs:
|
|
28
|
+
list: ${{ steps.s.outputs.list }}
|
|
29
|
+
steps:
|
|
30
|
+
- id: s
|
|
31
|
+
env: { JOBS: "${{ inputs.jobs }}" }
|
|
32
|
+
run: |
|
|
33
|
+
python3 - <<'EOF' >> "$GITHUB_OUTPUT"
|
|
34
|
+
import json, os
|
|
35
|
+
k = max(1, min(12, int(os.environ["JOBS"])))
|
|
36
|
+
print("list=" + json.dumps(list(range(1, k + 1))))
|
|
37
|
+
EOF
|
|
38
|
+
|
|
39
|
+
profile:
|
|
40
|
+
name: profile ${{ matrix.slot }}
|
|
41
|
+
needs: slots
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
timeout-minutes: 30
|
|
44
|
+
strategy:
|
|
45
|
+
fail-fast: false
|
|
46
|
+
matrix:
|
|
47
|
+
slot: ${{ fromJSON(needs.slots.outputs.list) }}
|
|
48
|
+
env: { N: "${{ inputs.n }}", REPS: "${{ inputs.reps }}" }
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v4
|
|
51
|
+
- uses: astral-sh/setup-uv@v6
|
|
52
|
+
- name: Machine
|
|
53
|
+
id: machine
|
|
54
|
+
run: |
|
|
55
|
+
{
|
|
56
|
+
lscpu | grep -E 'Model name|^CPU\(s\)|Thread|Core|Socket|MHz|L2|L3' || true
|
|
57
|
+
c++ --version | head -1
|
|
58
|
+
} | tee machine.txt
|
|
59
|
+
model=$(lscpu | sed -n 's/^Model name: *//p' | head -1)
|
|
60
|
+
case "$model" in
|
|
61
|
+
*Intel*|*INTEL*) vendor=intel ;;
|
|
62
|
+
*AMD*) vendor=amd ;;
|
|
63
|
+
*) vendor=other ;;
|
|
64
|
+
esac
|
|
65
|
+
echo "vendor=$vendor" >> "$GITHUB_OUTPUT"
|
|
66
|
+
echo "### profile ${{ matrix.slot }}: $model" >> "$GITHUB_STEP_SUMMARY"
|
|
67
|
+
- name: Allow hardware counters for this user
|
|
68
|
+
run: sudo sysctl -w kernel.perf_event_paranoid=1 || true
|
|
69
|
+
- name: Build fastprox
|
|
70
|
+
run: |
|
|
71
|
+
cmake -S . -B build/c -DCMAKE_BUILD_TYPE=Release
|
|
72
|
+
cmake --build build/c --target fastprox -j 4
|
|
73
|
+
- name: Build proxTV (fetched at run time, never vendored)
|
|
74
|
+
run: |
|
|
75
|
+
sudo apt-get update && sudo apt-get install -y liblapack-dev
|
|
76
|
+
uv sync --python 3.12
|
|
77
|
+
uv run python -c "import sys; sys.path.insert(0, 'bench'); import compare_external as c; print(c.build_proxtv())"
|
|
78
|
+
- name: Profile
|
|
79
|
+
run: |
|
|
80
|
+
c++ -O2 -std=c++17 bench/profile_kernels.cpp -Iinclude -Lbuild/c -lfastprox -ldl \
|
|
81
|
+
-Wl,-rpath,"$PWD/build/c" -o profile_kernels
|
|
82
|
+
./profile_kernels build/external/libproxtv_tv1.so "$N" "$REPS" | tee profile.txt
|
|
83
|
+
{ echo '```'; cat profile.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
|
|
84
|
+
- name: Assembly of the kernels (same flags as the library)
|
|
85
|
+
run: |
|
|
86
|
+
for f in tv1d projections; do
|
|
87
|
+
c++ -std=c++17 -O3 -DNDEBUG -fno-fast-math -Iinclude -Isrc -S "src/ops/$f.cpp" -o "$f.s"
|
|
88
|
+
done
|
|
89
|
+
python3 bench/asm_summary.py tv1d.s projections.s | tee asm_summary.txt
|
|
90
|
+
{ echo '```'; cat asm_summary.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
|
|
91
|
+
- uses: actions/upload-artifact@v4
|
|
92
|
+
with:
|
|
93
|
+
name: profile-${{ matrix.slot }}-${{ steps.machine.outputs.vendor }}
|
|
94
|
+
path: |
|
|
95
|
+
machine.txt
|
|
96
|
+
profile.txt
|
|
97
|
+
asm_summary.txt
|
|
98
|
+
tv1d.s
|
|
99
|
+
projections.s
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
name: variants
|
|
2
|
+
|
|
3
|
+
# Variants of the 1D TV kernel side by side against proxTV, in one binary per
|
|
4
|
+
# job (bench/tv_variants.cpp), on GitHub's arm64 runner (Neoverse-N2) and on
|
|
5
|
+
# x86_64 jobs that land on random Intel or AMD CPUs. Comparing within a job
|
|
6
|
+
# keeps the CPU out of the comparison. Runs on every push to a branch named
|
|
7
|
+
# variants/<topic>, or by hand from the Actions tab.
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: ["variants/**"]
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
inputs:
|
|
13
|
+
x86_jobs:
|
|
14
|
+
description: "x86_64 jobs (each lands on a random CPU), 0 to 8"
|
|
15
|
+
type: string
|
|
16
|
+
default: "3"
|
|
17
|
+
n:
|
|
18
|
+
description: "Samples per row"
|
|
19
|
+
type: string
|
|
20
|
+
default: "100000"
|
|
21
|
+
reps:
|
|
22
|
+
description: "Repetitions per setting (fresh input each)"
|
|
23
|
+
type: string
|
|
24
|
+
default: "31"
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
matrix:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
outputs:
|
|
30
|
+
include: ${{ steps.m.outputs.include }}
|
|
31
|
+
steps:
|
|
32
|
+
- id: m
|
|
33
|
+
env: { X86: "${{ inputs.x86_jobs || '3' }}" }
|
|
34
|
+
run: |
|
|
35
|
+
python3 - <<'EOF' >> "$GITHUB_OUTPUT"
|
|
36
|
+
import json, os
|
|
37
|
+
k = max(0, min(8, int(os.environ["X86"])))
|
|
38
|
+
jobs = [{"os": "ubuntu-24.04-arm", "slot": 0}] + [{"os": "ubuntu-latest", "slot": i} for i in range(1, k + 1)]
|
|
39
|
+
print("include=" + json.dumps(jobs))
|
|
40
|
+
EOF
|
|
41
|
+
|
|
42
|
+
variants:
|
|
43
|
+
name: variants ${{ matrix.os }} ${{ matrix.slot }}
|
|
44
|
+
needs: matrix
|
|
45
|
+
runs-on: ${{ matrix.os }}
|
|
46
|
+
timeout-minutes: 30
|
|
47
|
+
strategy:
|
|
48
|
+
fail-fast: false
|
|
49
|
+
matrix:
|
|
50
|
+
include: ${{ fromJSON(needs.matrix.outputs.include) }}
|
|
51
|
+
env: { N: "${{ inputs.n || '100000' }}", REPS: "${{ inputs.reps || '31' }}" }
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
- uses: astral-sh/setup-uv@v6
|
|
55
|
+
- name: Machine
|
|
56
|
+
id: machine
|
|
57
|
+
run: |
|
|
58
|
+
{
|
|
59
|
+
lscpu | grep -E 'Model name|^CPU\(s\)|Thread|Core|Socket|MHz|L2|L3' || true
|
|
60
|
+
c++ --version | head -1
|
|
61
|
+
} | tee machine.txt
|
|
62
|
+
model=$(lscpu | sed -n 's/^Model name: *//p' | head -1)
|
|
63
|
+
case "$model" in
|
|
64
|
+
*Intel*|*INTEL*) cpu=intel ;;
|
|
65
|
+
*AMD*) cpu=amd ;;
|
|
66
|
+
*Neoverse*) cpu=neoverse ;;
|
|
67
|
+
*) cpu=other ;;
|
|
68
|
+
esac
|
|
69
|
+
echo "cpu=$cpu" >> "$GITHUB_OUTPUT"
|
|
70
|
+
echo "### variants ${{ matrix.slot }}: $model" >> "$GITHUB_STEP_SUMMARY"
|
|
71
|
+
- name: Build proxTV (fetched at run time, never vendored)
|
|
72
|
+
run: |
|
|
73
|
+
sudo apt-get update && sudo apt-get install -y liblapack-dev
|
|
74
|
+
uv sync --python 3.12
|
|
75
|
+
uv run python -c "import sys; sys.path.insert(0, 'bench'); import compare_external as c; print(c.build_proxtv())"
|
|
76
|
+
- name: Variants
|
|
77
|
+
run: |
|
|
78
|
+
c++ -std=c++17 -O3 -DNDEBUG -fno-fast-math -Iinclude -Isrc bench/tv_variants.cpp \
|
|
79
|
+
src/api/api.cpp src/core/parallel.cpp src/ops/pointwise.cpp src/ops/group.cpp \
|
|
80
|
+
src/ops/projections.cpp src/ops/isotonic.cpp -DFASTPROX_BUILDING \
|
|
81
|
+
-DFASTPROX_VERSION_STRING='"variants"' -ldl -lpthread -o tv_variants
|
|
82
|
+
./tv_variants build/external/libproxtv_tv1.so "$N" "$REPS" | tee variants.txt
|
|
83
|
+
{ echo '```'; cat variants.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
|
|
84
|
+
- uses: actions/upload-artifact@v4
|
|
85
|
+
with:
|
|
86
|
+
name: variants-${{ matrix.slot }}-${{ steps.machine.outputs.cpu }}
|
|
87
|
+
path: |
|
|
88
|
+
machine.txt
|
|
89
|
+
variants.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,84 @@
|
|
|
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]
|
|
7
|
+
|
|
8
|
+
## [0.1.0] — 2026-10-16
|
|
9
|
+
|
|
10
|
+
First release. Changes since 0.1.0rc2:
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `variants` workflow and `bench/tv_variants.cpp`: variants of the 1D TV kernel side by side against proxTV (classic, linearized, hybrid and weighted taut string), compiled into one binary and timed on the same inputs with the order rotated, on the arm64 runner and on x86_64 jobs. It runs on pushes to `variants/*` branches or by hand. `condat` takes the length of its first, paper-form phase as a template parameter (`kShort` by default) so variants need no copy of the kernel.
|
|
15
|
+
|
|
16
|
+
- `profile` workflow (manual dispatch): several identical jobs on GitHub's x86_64 runner, each labelled with the CPU it landed on. Each job profiles single-thread 1D TV against proxTV with `bench/profile_kernels.cpp`, which records hardware counters where the VM exposes them and rotates the solver order so that none benefits from branch history. It also summarises GCC's assembly of the kernels (conditional jumps against selects) with `bench/asm_summary.py`.
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
- `examples/`: basic usage, 1D TV denoising, proximal gradient (lasso and batched simplex least squares), derivatives checked against finite differences, and bilevel learning of TV weights through the VJP. `tests/python/test_examples.py` runs them all; matplotlib for the plot is in the `examples` dependency group.
|
|
21
|
+
- `docs/api.md` lists, for every operator, which Jacobian the derivatives use at kinks and ties. It explains why each choice is an element of the B-subdifferential, and that the x, γ and parameter derivatives together form one. The ℓ∞ prox and conjugate proxes reverse the thresholding rule through the Moreau identity.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `bench/compare_external.py` rotates the order of the solvers with every repetition (fastprox's default solver always ran first, on an input just written, and its Condat path right after it), and quick mode takes the median of 7 repetitions instead of 3.
|
|
26
|
+
- 1D TV: the per-call finiteness and range pass runs in 8 independent lanes with `fminnm`/`fmaxnm` on arm64 (compare-selects elsewhere) instead of one dependent chain: 0.25 instead of 0.48 ns per sample with Clang and 0.89 with GCC on Apple M5 (float64, n = 10⁶). Weighted rows on white noise at λ = 10 went from 5.52 to 5.29 ns per sample, and float32 rows at λ = 0.1 are now 1–5% behind Condat's single-precision code (were 6–12%).
|
|
27
|
+
- 1D TV: the branch-free bound updates in short segments of Condat's algorithm are written as min/max of the excess instead of selects on a comparison, which GCC compiled back into branches. Same arithmetic up to the sign of zero. With GCC 16 on Apple M5, 4–12% faster at λ = 0.1 and 5–27% faster at λ = 1 (uniform and weighted), within 3% of the Clang build at λ ≤ 1; at λ = 10, uniform rows lose 1–19% under GCC (random walk most), where the branches it emitted happened to predict well, and weighted rows gain up to 10%. Clang's code is unchanged.
|
|
28
|
+
- Simplex and ℓ1 ball: the first pass of Condat's method skips blocks of 8 entries whose maximum is at or below the running pivot (the pivot can only change on an entry above it, so results are bit-identical). The ℓ1 norm that decides whether x is inside the ball is summed in 8 lanes instead of one dependent chain. On Apple M5 at n = 10⁶: simplex projection 0.50 instead of 0.60 ns per sample on Gaussian input, now faster than Condat's code (0.56); unweighted ℓ1 ball 0.64 instead of 1.08, weighted 1.08 instead of 1.37 (Clang) and 1.05 instead of 1.68 (GCC). The ℓ∞ prox shares the ℓ1-ball kernel.
|
|
29
|
+
|
|
30
|
+
- x86-64: the min/max helpers behind 1D TV's bound updates and range pass and the simplex block maximum use SSE2 `minsd`/`maxsd` instead of a compare-select, which GCC compiled into compare-and-branch. Same values. On GitHub's x86_64 runners (GCC 13.3, n = 10⁵), 1D TV went from fastest or within 2% of proxTV in 3–6 of 18 settings to 12–13 on Intel Xeons (8573C, 6973P-C) and from 14–17 to 17–18 on AMD EPYC; at λ = 1 fastprox now takes 0.82–1.03 of proxTV's time on Intel (was 1.01–1.16). Uniform random walk at λ = 10 lost 5–11% relative to proxTV, where the branches predicted well.
|
|
31
|
+
|
|
32
|
+
- 1D TV: after the first 3 samples of a segment, Condat's algorithm now tracks the segment by its running sum. The lower bound is the running maximum of the candidates (s − λₖ)/m and the upper bound the running minimum of (s + λₖ)/m, so the clamps are min/max and the only value carried between steps is the sum, instead of a dependency chain through every clamp (same algorithm and structure; values differ at rounding level). On Apple M5 (n = 10⁵, single thread) the kernel is 1.3–2.9× faster at λ ≥ 1 and unchanged at λ = 0.1. Weighted white noise at λ = 10, the one setting where fastprox trailed proxTV on every machine, went from 1.03–1.09 of proxTV's time to 0.43–0.45 (Clang) and from 1.14 to 0.60 (GCC 16), and uniform random walk at λ = 10 from 0.99 to 0.64. On GitHub's runners (GCC 13.3): weighted white noise at λ = 10 from 1.12–1.14 to 0.73–0.78 on Intel Xeons, from 1.00–1.08 to 0.55–0.62 on AMD EPYC and from 1.22 to 0.92 on Neoverse-N2; uniform random walk at λ = 10 from 1.05–1.09 to 0.83–0.84 on Intel and from 1.19 to 1.06 on Neoverse-N2. At λ = 0.1 weighted rows lost about 3% on Intel. Long segments no longer use branches for the clamps, so `kBranchFree` (8) is replaced by `kShort` (3).
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Weighted ℓ1-ball projection with weights spanning many orders of magnitude could return outputs far larger than the input (z = [5.5e7, 2.4e70] for x = [11.0, 2.0], weights [1.8e-308, 7.7e-246] and radius 1e-300; found by fuzzing). Removing an entry within rounding of the pivot that dominated the running sums cancelled them. The threshold is now kept at least at the largest breakpoint left out of the support, as it is in exact arithmetic. The same guard is in the unweighted kernel.
|
|
37
|
+
|
|
38
|
+
## [0.1.0rc2] — 2026-10-15
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- The macOS wheel of 0.1.0rc1 aborted the process (`OMP: Error #15`) when PyTorch, or any package bundling a recent LLVM OpenMP runtime, was used in the same process: each wheel carried its own libomp, and the copies refuse to coexist.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- Rows run on fastprox's own thread pool (`src/core/parallel.cpp`) instead of OpenMP. No OpenMP runtime is linked or bundled on any platform; the wheels depend only on the system and NumPy, and macOS wheels now support macOS 11.0 (arm64) and 10.15 (x86_64). Same results bit for bit; dynamic scheduling as before. Measured against the OpenMP build on Apple M5 in one process: equal on one thread, 1–8% slower at 2–4 threads on small batches, 8–31% faster at 8–15 threads.
|
|
47
|
+
- `threads = 0` means `fp_default_threads()`: `FASTPROX_NUM_THREADS`, else the first number in `OMP_NUM_THREADS`, else the CPUs the process may use (CPU affinity on Linux). Concurrent callers do not oversubscribe: one uses the pool, the others run serially. A child after `fork()` builds its own pool.
|
|
48
|
+
- `fp_has_openmp()` and `fastprox.has_openmp()` are replaced by `fp_default_threads()` and `fastprox.default_threads()`; the CMake option `FASTPROX_OPENMP` is now `FASTPROX_THREADS`.
|
|
49
|
+
- CI: a ThreadSanitizer job; AddressSanitizer and Valgrind now run with threads.
|
|
50
|
+
|
|
51
|
+
## [0.1.0rc1] — 2026-10-15
|
|
52
|
+
|
|
53
|
+
First release candidate, on PyPI (`pip install --pre fastprox`).
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- 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.
|
|
58
|
+
- 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).
|
|
59
|
+
- JVP and VJP in the input, the step and the operator parameters for every operator.
|
|
60
|
+
- 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.
|
|
61
|
+
- Python package built with nanobind (abi3, CPython ≥ 3.12): one-shot functions and operator classes with `prox`, `prox_conj`, `jvp` and `vjp`.
|
|
62
|
+
- C/C++ and Python test suites, a benchmark script, and a CI workflow.
|
|
63
|
+
- 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.
|
|
64
|
+
- 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.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
|
|
68
|
+
- 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.
|
|
69
|
+
- 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).
|
|
70
|
+
- 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.
|
|
71
|
+
- 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‖∞.
|
|
72
|
+
- 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.
|
|
73
|
+
- 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.
|
|
74
|
+
- 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.
|
|
75
|
+
- 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.
|
|
76
|
+
- The version is defined once, in `CMakeLists.txt`; the Python package reads it from there.
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
|
|
80
|
+
- 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.
|
|
81
|
+
- `fp_prox_conj` with σ = 0 on arrays with negative strides computed byte offsets in unsigned arithmetic (undefined behaviour, found by fuzzing).
|
|
82
|
+
- Static Windows builds marked API symbols `dllexport`.
|
|
83
|
+
- 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.
|
|
84
|
+
- 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.
|
fastprox-0.1.0/CLAUDE.md
ADDED
|
@@ -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 -DCMAKE_BUILD_TYPE=Debug` (ThreadSanitizer: `-fsanitize=thread` in the C/C++ and linker flags, as in the `tsan` CI job)
|
|
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, saved structure; `src/core/parallel.cpp` — the thread pool (no OpenMP: bundled libomp copies abort each other, e.g. next to PyTorch); `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`.
|