Skip to content

Repository files navigation

scipycpp

License: MITC++17

Overview

scipycpp is a header-only C++ library implementing scipy's core APIs with bit-level precision alignment against Python scipy.

Built on proven C++ libraries:

  • numpycpp — numpy primitives
  • Eigen3 — linear algebra (solve, inv, det, eig, svd, cholesky)
  • pocketfft (bundled) — FFT (same library numpy/scipy use internally)
  • ckdtree (bundled) — KDTree from scipy.spatial

Quick Start

#include"scipy/core.h"// Integrationauto [val, err] = scipy::integrate::quad([](double x){ return x*x; }, 0.0, 4.0);
// Stats
std::vector<double> pdf(3);
scipy::stats::norm_pdf(data, pdf.data(), 3);
// Linalg (Eigen3-backed)double A[4] = {2,1,1,3}, b[2] = {5,6}, x[2];
scipy::linalg::solve(A, x, b, 2);
// FFT (pocketfft-backed — same as numpy/scipy)scipy::fft::fft(signal, spectrum, 4);
// Spatial — KDTree with distances
scipy::spatial::KDTree<double> tree(points, n_pts, dim);
double dist; size_t idx;
tree.query(query_point, dist, idx); // returns distance AND index// Spatial — cross-set distance matrix (cdist)scipy::spatial::distance::cdist(XA, mA, XB, mB, dim, dm);
// ndimage — 1D Gaussian filter
std::vector<double> out(n);
scipy::ndimage::gaussian_filter1d(src, out.data(), n, sigma);
// Signal — median filterscipy::signal::medfilt(src, dst.data(), n, kernel_size);
// Transform — Rotation (from_matrix, from_euler, as_euler, as_matrix)auto rot = scipy::spatial::transform::Rotation<double>::from_matrix(R);
auto euler = rot.as_euler_vec("xyz"); // returns [rx, ry, rz]// from_euler + as_matrix — single axis (key use case: ego yaw)double m9[9];
auto rot_z = scipy::spatial::transform::Rotation<double>::from_euler("z", yaw);
rot_z.as_matrix(m9); // 3×3 row-major rotation matrix// from_euler — multi-axis extrinsic / intrinsicdouble angles[3] = {rx, ry, rz};
auto rot_xyz = scipy::spatial::transform::Rotation<double>::from_euler("xyz", angles);
rot_xyz.as_matrix(m9);

Dependencies

# Install dependencies
sudo dpkg -i numpycpp-dev-*.deb
sudo apt-get install libeigen3-dev
# Install scipycpp
mkdir build &&cd build
cmake .. && make deb
sudo dpkg -i scipycpp-dev-*.deb
find_package(scipycppREQUIRED)
target_link_libraries(myappPRIVATEscipycpp::scipycpp)

Modules & ULP Alignment

Under the bitexact build (numpycpp dlsym backend, same math kernels as scipy), all APIs are 0 ULP bit-identical for both float64/float32, including extreme values (±inf, NaN, ±0.0, subnormals, saturation inputs).

ModuleBackendKey APIsULP Status
statsCephes + numpcpp npy_exp (dlsym)norm.pdf, norm.cdf, norm.ppf0 ULP
integratesequential C++ sumtrapezoid, simpson⚠️0 ULP typical; ≤6 ULP uniform arrays
linalgEigen3 partialPivLusolve⚠️ ULP-tolerant (Eigen3 vs LAPACK)
spatialpure C++ / ckdtreecdist, KDTree0 ULP
ndimagenumpy kernel + C++ convolutiongaussian_filter1d0 ULP
signalsort-based medianmedfilt0 ULP
transformpure C++ (no scipy delegation)Rotation.from_matrix, from_euler, as_euler, as_matrix0 ULP single-axis; ≤200 ULP multi-axis

Full per-test ULP report: doc/ulp_report.csv (Auto-generated by pytest tests/test_all.py, see doc/ulp_report.md for summary)

Why some non-zero ULPs for linalg.solve?

APIMax ULPRoot Cause
linalg.solve≤8.9e4Eigen3 partialPivLu vs LAPACK gesv: different LU pivots produce different roundoff paths. Well-conditioned small matrices (2×2, identity) are bit-identical. All results within atol=1e-14 (ill-conditioned: atol=1e-10).

Integrate ULP alignment detail

APIInputMax ULPRoot Cause
trapezoid / simpsontypical scientific data0 ULPSequential sum matches numpy pairwise for non-uniform data
trapezoidfloat64 uniform array≤5 f64-ULPSequential vs SIMD pairwise reorder (unavoidable without SIMD intrinsics)
trapezoidfloat32 uniform array≤4 f32-ULPSame, measured in native float32 precision
simpsonfloat64 uniform array≤6 f64-ULPSame
simpsonfloat32 input (any)≤6 f32-ULPscipy computes internal sum in float32 SIMD; C++ uses sequential float32

scipy.integrate.simpson always returns float64 regardless of input dtype (Python float constants promote the result). scipy::integrate::simpson<T> matches this: it computes the intermediate sum in T (preserving scipy's precision path), then multiplies by 1.0/3.0 in double and returns double.

norm.pdf: numpycpp v1.21.2+ resolves exp via dlsym("npy_exp"), matching scipy's internal numpy math path bit-for-bit.

Testing

Bit-level alignment tests against Python scipy. Build flags mirror the numpycpp README bit-exact backend spec exactly.

# Build (cmake with bit-exact flags: -O2 -ffp-contract=off -msse4.1 -mfma, …)
cmake -S tests -B tests/build
cmake --build tests/build -j$(nproc)# Run alignment testscd tests && python3 -m pytest test_all.py -q --tb=short --no-header
# ULP report printed to stderr + exported to doc/ulp_report.csv

Summary: doc/ulp_report.md — auto-generated aggregate of doc/ulp_report.csv.

License

MIT

About

Header-only C++17 scipy API mirror — based on numpcpp+Eigen3+pocketfft, bit-exact alignment

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages