-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodule.cpp
More file actions
327 lines (310 loc) · 17.8 KB
/
Copy pathmodule.cpp
File metadata and controls
327 lines (310 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include "../pycpp/stats_py.h"
#include "../pycpp/integrate_py.h"
#include "../pycpp/linalg_py.h"
#include "../pycpp/spatial_py.h"
#include "../pycpp/ndimage_py.h"
#include "../pycpp/signal_py.h"
#include "../pycpp/transform_py.h"
#include "numpycpp/numpy.h"
namespace py = pybind11;
PYBIND11_MODULE(scipycpp, m) {
m.doc() = "C++ bit-level alignment of Python scipy APIs";
// numpcpp auto-discovers numpy's _multiarray_umath.so via /proc/self/maps.
// No explicit bridge_init() needed — lazy init on first numpy::exp() call.
// ====================================================================
// stats submodule — norm.pdf
// ====================================================================
py::module_ stats = m.def_submodule("stats", "scipy.stats equivalents");
py::class_<int> norm_cls(stats, "norm", "Frozen normal distribution");
norm_cls.def_static("pdf",
[](const py::array& x, py::object loc, py::object scale) -> py::object {
auto buf = x.request();
if (buf.format == py::format_descriptor<double>::format()) {
double _loc = loc.is_none() ? 0.0 : loc.cast<double>();
double _scale = scale.is_none() ? 1.0 : scale.cast<double>();
return scipy::stats::norm_pdf<double>(x.cast<py::array_t<double>>(), _loc, _scale);
} else {
// Float32 input: scipy promotes to float64 and returns float64.
// Compute z=(x-loc)/scale in float32, promote to float64, compute pdf, return float64.
float _loc = loc.is_none() ? 0.0f : loc.cast<float>();
float _scale = scale.is_none() ? 1.0f : scale.cast<float>();
size_t n = static_cast<size_t>(buf.size);
py::array_t<double> result(buf.shape);
auto* d_r = static_cast<double*>(result.request().ptr);
const auto* f_x = static_cast<const float*>(buf.ptr);
if (_loc == 0.0f && _scale == 1.0f) {
for (py::ssize_t i = 0; i < buf.size; ++i)
d_r[i] = static_cast<double>(f_x[i]);
scipy::stats::norm_pdf(static_cast<const double*>(result.request().ptr),
static_cast<double*>(result.request().ptr), n);
} else {
double d_loc = static_cast<double>(_loc);
double d_scale = static_cast<double>(_scale);
for (py::ssize_t i = 0; i < buf.size; ++i)
d_r[i] = static_cast<double>((f_x[i] - _loc) / _scale);
scipy::stats::norm_pdf(static_cast<const double*>(result.request().ptr),
static_cast<double*>(result.request().ptr), n);
// Division order matches scipy: _pdf(z) / scale (NOT _pdf(z) * (1/scale))
for (py::ssize_t i = 0; i < buf.size; ++i)
d_r[i] /= d_scale;
}
return result;
}
},
py::arg("x"), py::arg("loc") = py::none(), py::arg("scale") = py::none(),
"Probability density function.");
norm_cls.def_static("cdf",
[](const py::array& x, py::object loc, py::object scale) -> py::object {
auto buf = x.request();
if (buf.format == py::format_descriptor<double>::format()) {
double _loc = loc.is_none() ? 0.0 : loc.cast<double>();
double _scale = scale.is_none() ? 1.0 : scale.cast<double>();
py::array_t<double> result(buf.shape);
if (_loc == 0.0 && _scale == 1.0)
scipy::stats::norm_cdf(static_cast<const double*>(buf.ptr),
static_cast<double*>(result.request().ptr), buf.size);
else
scipy::stats::norm_cdf(static_cast<const double*>(buf.ptr),
static_cast<double*>(result.request().ptr), buf.size, _loc, _scale);
return result;
} else {
// Float32 input: scipy promotes to float64 internally and returns float64.
// Match this by promoting, computing in float64, returning float64.
float _loc = loc.is_none() ? 0.0f : loc.cast<float>();
float _scale = scale.is_none() ? 1.0f : scale.cast<float>();
size_t n = static_cast<size_t>(buf.size);
py::array_t<double> result(buf.shape);
auto* d_res = static_cast<double*>(result.request().ptr);
const auto* f_x = static_cast<const float*>(buf.ptr);
// Compute z = (x - loc) / scale in float32, then promote to float64
for (py::ssize_t i = 0; i < buf.size; ++i)
d_res[i] = static_cast<double>((f_x[i] - _loc) / _scale);
// Compute cdf in float64
scipy::stats::norm_cdf(static_cast<const double*>(result.request().ptr),
static_cast<double*>(result.request().ptr), n);
return result;
}
},
py::arg("x"), py::arg("loc") = py::none(), py::arg("scale") = py::none(),
"Cumulative distribution function.");
norm_cls.def_static("ppf",
[](const py::array& x, py::object loc, py::object scale) -> py::object {
auto buf = x.request();
if (buf.format == py::format_descriptor<double>::format()) {
double _loc = loc.is_none() ? 0.0 : loc.cast<double>();
double _scale = scale.is_none() ? 1.0 : scale.cast<double>();
py::array_t<double> result(buf.shape);
if (_loc == 0.0 && _scale == 1.0)
scipy::stats::norm_ppf(static_cast<const double*>(buf.ptr),
static_cast<double*>(result.request().ptr), buf.size);
else
scipy::stats::norm_ppf(static_cast<const double*>(buf.ptr),
static_cast<double*>(result.request().ptr), buf.size, _loc, _scale);
return result;
} else {
// Float32 input: scipy computes ndtri in float64, truncates to float32,
// then applies loc/scale entirely in float32 arithmetic.
float _loc = loc.is_none() ? 0.0f : loc.cast<float>();
float _scale = scale.is_none() ? 1.0f : scale.cast<float>();
size_t n = static_cast<size_t>(buf.size);
// Step 1: promote p to float64, compute ndtri in float64
py::array_t<double> tmp64(buf.shape);
auto* d_tmp = static_cast<double*>(tmp64.request().ptr);
const auto* f_p = static_cast<const float*>(buf.ptr);
for (py::ssize_t i = 0; i < buf.size; ++i)
d_tmp[i] = static_cast<double>(f_p[i]);
scipy::stats::norm_ppf(static_cast<const double*>(tmp64.request().ptr),
static_cast<double*>(tmp64.request().ptr), n);
// Step 2: truncate ndtri to float32, apply loc/scale in float32
py::array_t<double> result(buf.shape);
auto* d_res = static_cast<double*>(result.request().ptr);
for (py::ssize_t i = 0; i < buf.size; ++i) {
float x = static_cast<float>(d_tmp[i]);
d_res[i] = static_cast<double>(_scale * x + _loc);
}
return result;
}
},
py::arg("x"), py::arg("loc") = py::none(), py::arg("scale") = py::none(),
"Percent point function (inverse of cdf).");
// ====================================================================
// integrate — trapezoid, simpson
//
// Thin wrappers that call the actual C++ implementations in
// scipy/integrate.h via integrate_py.h. The C++ code uses sequential
// summation; scipy uses numpy.add.reduce (SIMD pairwise). For typical
// scientific arrays these agree to 0 ULP; for degenerate uniform arrays
// of n~100 elements the difference is ≤6 ULP (documented in README).
// ====================================================================
m.def("trapezoid",
[](py::array_t<double, py::array::c_style | py::array::forcecast> y) -> double {
return scipy::integrate::trapezoid_py<double>(y);
});
m.def("trapezoid",
[](py::array_t<float, py::array::c_style | py::array::forcecast> y) -> float {
return scipy::integrate::trapezoid_py<float>(y);
});
m.def("simpson",
[](py::array_t<double, py::array::c_style | py::array::forcecast> y) -> double {
return scipy::integrate::simpson_py<double>(y);
});
m.def("simpson",
[](py::array_t<float, py::array::c_style | py::array::forcecast> y) -> double {
// simpson always returns double (scipy always returns float64)
return scipy::integrate::simpson_py<float>(y);
});
// ====================================================================
// linalg — solve
// ====================================================================
// scipy.linalg.solve delegates to LAPACK gesv which operates in float64
// precision internally (even for float32 inputs, via temporary promotion).
// We mirror this: float32 inputs are promoted, solved in float64, returned
// as float64 for bit-level alignment with scipy.
py::module_ la = m.def_submodule("linalg", "scipy.linalg equivalents");
la.def("solve",
[](const py::array& A, const py::array& b) -> py::object {
auto ba = A.request();
if (ba.format == py::format_descriptor<double>::format()) {
return scipy::linalg::solve_py<double>(
A.cast<py::array_t<double>>(), b.cast<py::array_t<double>>());
} else {
// Float32 input: promote to float64, solve in double precision,
// return float64. Matches scipy's internal LAPACK promotion.
auto f32_A = A.cast<py::array_t<float>>();
auto f32_b = b.cast<py::array_t<float>>();
auto bufA = f32_A.request(), bufB = f32_b.request();
py::ssize_t n = bufA.shape[0];
py::array_t<double> A64(bufA.shape);
py::array_t<double> b64(bufB.shape);
const float* pa = static_cast<const float*>(bufA.ptr);
const float* pb = static_cast<const float*>(bufB.ptr);
double* da = static_cast<double*>(A64.request().ptr);
double* db = static_cast<double*>(b64.request().ptr);
for (py::ssize_t i = 0; i < bufA.size; ++i) da[i] = static_cast<double>(pa[i]);
for (py::ssize_t i = 0; i < bufB.size; ++i) db[i] = static_cast<double>(pb[i]);
return scipy::linalg::solve_py<double>(A64, b64);
}
});
// ====================================================================
// spatial submodule — cdist, KDTree
// ====================================================================
py::module_ sp = m.def_submodule("spatial", "scipy.spatial equivalents");
py::module_ sp_dist = sp.def_submodule("distance", "scipy.spatial.distance equivalents");
sp_dist.def("cdist",
[](const py::array& XA, const py::array& XB,
const std::string& metric) -> py::object {
auto ba = XA.request();
if (ba.format == py::format_descriptor<double>::format()) {
return scipy_py::spatial::cdist<double>(
XA.cast<py::array_t<double>>(), XB.cast<py::array_t<double>>(), metric);
} else {
// Float32 input: scipy promotes to float64 internally, returns float64.
// Promote inputs to float64, compute in double precision.
auto f32_XA = XA.cast<py::array_t<float>>();
auto f32_XB = XB.cast<py::array_t<float>>();
auto bufA = f32_XA.request(), bufB = f32_XB.request();
py::array_t<double> XA64(bufA.shape);
py::array_t<double> XB64(bufB.shape);
const float* pa = static_cast<const float*>(bufA.ptr);
const float* pb = static_cast<const float*>(bufB.ptr);
double* da = static_cast<double*>(XA64.request().ptr);
double* db = static_cast<double*>(XB64.request().ptr);
for (py::ssize_t i = 0; i < bufA.size; ++i) da[i] = static_cast<double>(pa[i]);
for (py::ssize_t i = 0; i < bufB.size; ++i) db[i] = static_cast<double>(pb[i]);
return scipy_py::spatial::cdist<double>(XA64, XB64, metric);
}
},
py::arg("XA"), py::arg("XB"), py::arg("metric") = "euclidean",
"Cross-set distance matrix. Aligns with scipy.spatial.distance.cdist()");
// KDTree: register both float64 and float32 wrappers.
// ckdtree internally works with double; float32 inputs are auto-converted.
scipy_py::spatial::bind_kdtree<double>(sp, "KDTree");
scipy_py::spatial::bind_kdtree<float>(sp, "KDTree_f32");
// ====================================================================
// ndimage — gaussian_filter1d
// ====================================================================
// To bit-level align with scipy's gaussian_filter1d:
// 1. Compute half-width as int(truncate * sigma + 0.5)
// 2. Build Gaussian kernel using numpy.exp + numpy.sum (SVML + SIMD sum)
// 3. Call C++ gaussian_filter_correlate with pre-computed kernel
py::module_ ndi = m.def_submodule("ndimage", "scipy.ndimage equivalents");
ndi.def("gaussian_filter1d",
[](const py::array& input, double sigma, double truncate,
const std::string& mode, double cval) -> py::object {
auto buf = input.request();
int mode_int = 0;
if (mode == "reflect") mode_int = 0;
else if (mode == "constant") mode_int = 1;
else if (mode == "nearest") mode_int = 2;
else if (mode == "mirror") mode_int = 3;
else if (mode == "wrap") mode_int = 4;
// Compute kernel half-width (matching scipy)
int half = static_cast<int>(truncate * sigma + 0.5);
if (half < 1) half = 1;
double sigma2 = sigma * sigma;
// Build kernel using numpy (SVML exp + numpy.sum for bit-level alignment)
py::object np = py::module_::import("numpy");
py::object x = np.attr("arange")(-half, half + 1);
py::object phi_x = np.attr("exp")(py::float_(-0.5 / sigma2) * (x * x));
py::object kernel = phi_x / np.attr("sum")(phi_x);
auto kernel_buf = kernel.cast<py::array_t<double>>().request();
if (buf.format == py::format_descriptor<double>::format()) {
auto input_arr = input.cast<py::array_t<double>>();
py::array_t<double> result(buf.shape);
scipy::ndimage::gaussian_filter_correlate<double>(
static_cast<const double*>(input_arr.request().ptr),
static_cast<double*>(result.request().ptr),
buf.size,
static_cast<const double*>(kernel_buf.ptr),
half, mode_int, cval);
return result;
} else {
// Float32: scipy computes internally in float64, rounds to float32
auto f32_arr = input.cast<py::array_t<float>>();
auto f32_buf = f32_arr.request();
std::vector<double> src64(buf.size);
const float* sf = static_cast<const float*>(f32_buf.ptr);
for (size_t i = 0; i < buf.size; ++i) src64[i] = static_cast<double>(sf[i]);
std::vector<double> dst64(buf.size);
scipy::ndimage::gaussian_filter_correlate<double>(
src64.data(), dst64.data(), buf.size,
static_cast<const double*>(kernel_buf.ptr),
half, mode_int, cval);
py::array_t<float> result(buf.shape);
float* df = static_cast<float*>(result.request().ptr);
for (size_t i = 0; i < buf.size; ++i) df[i] = static_cast<float>(dst64[i]);
return result;
}
},
py::arg("input"), py::arg("sigma") = 1.0, py::arg("truncate") = 4.0,
py::arg("mode") = "reflect", py::arg("cval") = 0.0,
"1-D Gaussian filter. Aligns with scipy.ndimage.gaussian_filter1d()");
// ====================================================================
// signal — medfilt
// ====================================================================
py::module_ sig = m.def_submodule("signal", "scipy.signal equivalents");
sig.def("medfilt",
[](const py::array& volume, int kernel_size) -> py::object {
auto buf = volume.request();
if (buf.format == py::format_descriptor<double>::format()) {
return scipy_py::signal::medfilt<double>(
volume.cast<py::array_t<double>>(), kernel_size);
} else {
return scipy_py::signal::medfilt<float>(
volume.cast<py::array_t<float>>(), kernel_size);
}
},
py::arg("volume"), py::arg("kernel_size") = 3,
"1D median filter. Aligns with scipy.signal.medfilt()");
// ====================================================================
// spatial.transform — Rotation
// ====================================================================
// Pure C++ implementation — zero scipy delegation (Issue 001 fix).
// from_matrix / as_euler / from_euler / as_matrix all use C++ path.
// sp_Rotation is no longer needed; passed as py::none().
py::module_ sp_tf = sp.def_submodule("transform", "scipy.spatial.transform equivalents");
scipy_py::transform::bind_rotation<double>(sp_tf, "Rotation", py::none());
scipy_py::transform::bind_rotation<float>(sp_tf, "Rotation_f32", py::none());
}