Skip to content

Commit 4b4ed81

Browse files
district10zhixiong-tangclaude
authored
Bridge Eigen types to pocketpy ndarray for RDP binding; tested on python/pocketpy/wasm (#9)
* Bridge Eigen types to pocketpy ndarray for RDP binding Add rdp/rdp_mask bindings to the pocketpy numpy module by bridging Eigen matrix types with xtensor-backed ndarray via helper functions. Register a pocket_numpy module in main.cpp to re-export these functions. Fix star-unpacking syntax in test_rdp.py for pocketpy compatibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move pocket_numpy module registration into numpy.cpp The WASM build only compiles numpy.cpp (not main.cpp), so the pocket_numpy module must be registered inside the PYBIND11_MODULE block to work in both native and WASM builds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix pocket_numpy registration for WASM (no C++ exceptions) Use raw pocketpy C API (py_getattr/py_retval) instead of pkbind's m.attr() which relies on C++ throw — WASM/emscripten builds without -fexceptions would abort on throw, preventing the module from being created. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * update wasm * more test cases * fix --------- Co-authored-by: tang zhixiong <zhixiong.tang@momenta.ai> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 15bd08f commit 4b4ed81

File tree

7 files changed

+190
-14
lines changed

7 files changed

+190
-14
lines changed

build_wasm.sh

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ fi
99
rm -rf docs/lib
1010
mkdir -p docs/lib
1111

12-
# Generate test_numpy.js from test source
12+
# Generate test_sources.js from all test files
1313
python3 -c "
14-
import json
15-
with open('tests/test_numpy.py') as f:
16-
src = f.read()
17-
print('var TEST_SOURCE = ' + json.dumps(src) + ';')
18-
" > docs/test_numpy.js
14+
import json, glob, os
15+
sources = {}
16+
for path in sorted(glob.glob('tests/*.py')):
17+
name = os.path.basename(path)
18+
with open(path) as f:
19+
sources[name] = f.read()
20+
print('var TEST_SOURCES = ' + json.dumps(sources) + ';')
21+
" > docs/test_sources.js
1922

2023
# Common flags
2124
DEFINES="-DPK_ENABLE_OS=0 -DPK_ENABLE_THREADS=0 -DPK_ENABLE_DETERMINISM=0 \

docs/index.html

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,25 @@
7676
min-height: 50px;
7777
line-height: 1.4;
7878
}
79+
#file-select {
80+
font-family: monospace;
81+
font-size: 14px;
82+
padding: 4px 8px;
83+
margin-bottom: 6px;
84+
background-color: #282a36;
85+
color: #f8f8f2;
86+
border: 1px solid #444;
87+
border-radius: 4px;
88+
outline: none;
89+
}
7990
.pass { color: #4ec9b0; font-weight: bold; }
8091
.fail { color: #f44747; font-weight: bold; }
8192
</style>
8293
</head>
8394
<body>
8495
<h1>pocket-numpy</h1>
8596
GitHub: <a href="https://github.com/cubao/xtensor-numpy" style="color: #ffff00; text-decoration: none;" target="_blank">cubao/xtensor-numpy</a>
97+
<select id="file-select"></select>
8698
<div class="editor-container">
8799
<div class="line-numbers" id="line-numbers">1</div>
88100
<textarea id="editor" spellcheck="false"></textarea>
@@ -92,16 +104,31 @@ <h1>pocket-numpy</h1>
92104
<div id="status">Loading WASM module...</div>
93105
<pre id="output"></pre>
94106

95-
<script src="test_numpy.js"></script>
107+
<script src="test_sources.js"></script>
96108
<script>
97109
var editorEl = document.getElementById('editor');
98110
var outputEl = document.getElementById('output');
99111
var statusEl = document.getElementById('status');
100112
var runBtn = document.getElementById('run-btn');
101113
var ready = false;
102114

103-
// Fill editor with bundled test source
104-
editorEl.value = TEST_SOURCE;
115+
// Populate file selector dropdown
116+
var fileSelect = document.getElementById('file-select');
117+
var fileNames = Object.keys(TEST_SOURCES);
118+
for (var i = 0; i < fileNames.length; i++) {
119+
var opt = document.createElement('option');
120+
opt.value = fileNames[i];
121+
opt.textContent = fileNames[i];
122+
fileSelect.appendChild(opt);
123+
}
124+
125+
function loadSelectedFile() {
126+
editorEl.value = TEST_SOURCES[fileSelect.value];
127+
updateLineNumbers();
128+
autoResizeEditor();
129+
}
130+
131+
fileSelect.addEventListener('change', loadSelectedFile);
105132

106133
// Tab key inserts a tab instead of moving focus
107134
editorEl.addEventListener('keydown', function (e) {
@@ -157,9 +184,11 @@ <h1>pocket-numpy</h1>
157184
document.getElementById('line-numbers').scrollTop = this.scrollTop;
158185
});
159186

160-
// Initial line numbers and auto-resize
161-
updateLineNumbers();
162-
autoResizeEditor();
187+
// Load initial file and set up editor
188+
if (fileNames.indexOf('test_numpy.py') !== -1) {
189+
fileSelect.value = 'test_numpy.py';
190+
}
191+
loadSelectedFile();
163192

164193
// Re-calc on window resize
165194
window.addEventListener('resize', function() {

docs/lib/pocketpy.wasm

23.7 KB
Binary file not shown.

docs/test_numpy.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/test_sources.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/numpy.cpp

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include <pybind11/stl.h>
33
#include <numpy.hpp>
44
#include <typeinfo>
5+
#include "rdp.hpp"
56

67
namespace py = pybind11;
78

@@ -2868,6 +2869,88 @@ void array_creation_registry(py::module_& m) {
28682869
register_array_float<5>(m);
28692870
}
28702871

2872+
// --- RDP helpers: bridge ndarray/list <-> Eigen types ---
2873+
2874+
// Helper: extract Nx2 or Nx3 Eigen matrix from ndarray_base
2875+
static rdp::RowVectors ndarray_to_eigen(const ndarray_base& base, int& input_cols) {
2876+
int dims = base.ndim();
2877+
if (dims != 2) throw std::invalid_argument("rdp: expected 2D array");
2878+
auto shape_vec = base.shape();
2879+
int rows = shape_vec[0].cast<int>();
2880+
int cols = shape_vec[1].cast<int>();
2881+
if (cols != 2 && cols != 3) throw std::invalid_argument("rdp: expected Nx2 or Nx3 array");
2882+
input_cols = cols;
2883+
2884+
if (auto* p = dynamic_cast<const ndarray<float64>*>(&base)) {
2885+
const double* ptr = p->data.get_array().data();
2886+
if (cols == 3) {
2887+
return Eigen::Map<const rdp::RowVectors>(ptr, rows, 3);
2888+
} else {
2889+
rdp::RowVectors result(rows, 3);
2890+
result.setZero();
2891+
Eigen::Map<const rdp::RowVectorsNx2> map2(ptr, rows, 2);
2892+
result.leftCols(2) = map2;
2893+
return result;
2894+
}
2895+
}
2896+
if (auto* p = dynamic_cast<const ndarray<int_>*>(&base)) {
2897+
const int_* iptr = p->data.get_array().data();
2898+
rdp::RowVectors result(rows, 3);
2899+
result.setZero();
2900+
for (int i = 0; i < rows; ++i)
2901+
for (int j = 0; j < cols; ++j)
2902+
result(i, j) = static_cast<double>(iptr[i * cols + j]);
2903+
return result;
2904+
}
2905+
throw std::invalid_argument("rdp: unsupported ndarray dtype");
2906+
}
2907+
2908+
// Helper: convert vector<vector<double>> to Eigen Nx3 (zero-pad 2D to 3D)
2909+
static rdp::RowVectors vec2d_to_eigen_d(const std::vector<std::vector<double>>& coords, int& input_cols) {
2910+
int rows = (int)coords.size();
2911+
if (rows == 0) throw std::invalid_argument("rdp: empty coords");
2912+
int cols = (int)coords[0].size();
2913+
if (cols != 2 && cols != 3) throw std::invalid_argument("rdp: expected Nx2 or Nx3");
2914+
input_cols = cols;
2915+
rdp::RowVectors result(rows, 3);
2916+
result.setZero();
2917+
for (int i = 0; i < rows; ++i)
2918+
for (int j = 0; j < cols; ++j)
2919+
result(i, j) = coords[i][j];
2920+
return result;
2921+
}
2922+
2923+
// Helper: convert vector<vector<int_>> to Eigen Nx3
2924+
static rdp::RowVectors vec2d_to_eigen_i(const std::vector<std::vector<int_>>& coords, int& input_cols) {
2925+
int rows = (int)coords.size();
2926+
if (rows == 0) throw std::invalid_argument("rdp: empty coords");
2927+
int cols = (int)coords[0].size();
2928+
if (cols != 2 && cols != 3) throw std::invalid_argument("rdp: expected Nx2 or Nx3");
2929+
input_cols = cols;
2930+
rdp::RowVectors result(rows, 3);
2931+
result.setZero();
2932+
for (int i = 0; i < rows; ++i)
2933+
for (int j = 0; j < cols; ++j)
2934+
result(i, j) = static_cast<double>(coords[i][j]);
2935+
return result;
2936+
}
2937+
2938+
// Helper: Eigen RowVectors result -> ndarray_base (return Nx2 or Nx3)
2939+
static std::unique_ptr<ndarray_base> eigen_to_ndarray(const rdp::RowVectors& mat, int output_cols) {
2940+
int N = (int)mat.rows();
2941+
std::vector<std::vector<double>> data(N, std::vector<double>(output_cols));
2942+
for (int i = 0; i < N; ++i)
2943+
for (int j = 0; j < output_cols; ++j)
2944+
data[i][j] = mat(i, j);
2945+
return std::unique_ptr<ndarray_base>(new ndarray<float64>(data));
2946+
}
2947+
2948+
// Helper: Eigen VectorXi mask -> ndarray_base (1D int array)
2949+
static std::unique_ptr<ndarray_base> mask_to_ndarray(const Eigen::VectorXi& mask) {
2950+
std::vector<int_> data(mask.size());
2951+
for (int i = 0; i < mask.size(); ++i) data[i] = mask[i];
2952+
return std::unique_ptr<ndarray_base>(new ndarray<int_>(data));
2953+
}
28712954

28722955
PYBIND11_MODULE(numpy, m) {
28732956
m.doc() = "Python bindings for pkpy::numpy::ndarray using pybind11";
@@ -3535,4 +3618,65 @@ PYBIND11_MODULE(numpy, m) {
35353618
py::arg("arr2"),
35363619
py::arg("rtol") = 1e-5,
35373620
py::arg("atol") = 1e-8);
3621+
3622+
// --- RDP bindings ---
3623+
3624+
// rdp from list<list<double>>
3625+
m.def("rdp", [](std::vector<std::vector<double>> coords, double epsilon, bool recursive) -> std::unique_ptr<ndarray_base> {
3626+
int input_cols;
3627+
auto eigen_coords = vec2d_to_eigen_d(coords, input_cols);
3628+
auto result = rdp::douglas_simplify(eigen_coords, epsilon, recursive);
3629+
return eigen_to_ndarray(result, input_cols);
3630+
}, py::arg("coords"), py::arg("epsilon") = 0.0, py::arg("recursive") = true);
3631+
3632+
// rdp from list<list<int>>
3633+
m.def("rdp", [](std::vector<std::vector<int_>> coords, double epsilon, bool recursive) -> std::unique_ptr<ndarray_base> {
3634+
int input_cols;
3635+
auto eigen_coords = vec2d_to_eigen_i(coords, input_cols);
3636+
auto result = rdp::douglas_simplify(eigen_coords, epsilon, recursive);
3637+
return eigen_to_ndarray(result, input_cols);
3638+
}, py::arg("coords"), py::arg("epsilon") = 0.0, py::arg("recursive") = true);
3639+
3640+
// rdp from ndarray
3641+
m.def("rdp", [](const ndarray_base& coords, double epsilon, bool recursive) -> std::unique_ptr<ndarray_base> {
3642+
int input_cols;
3643+
auto eigen_coords = ndarray_to_eigen(coords, input_cols);
3644+
auto result = rdp::douglas_simplify(eigen_coords, epsilon, recursive);
3645+
return eigen_to_ndarray(result, input_cols);
3646+
}, py::arg("coords"), py::arg("epsilon") = 0.0, py::arg("recursive") = true);
3647+
3648+
// rdp_mask from list<list<double>>
3649+
m.def("rdp_mask", [](std::vector<std::vector<double>> coords, double epsilon, bool recursive) -> std::unique_ptr<ndarray_base> {
3650+
int input_cols;
3651+
auto eigen_coords = vec2d_to_eigen_d(coords, input_cols);
3652+
auto mask = rdp::douglas_simplify_mask(eigen_coords, epsilon, recursive);
3653+
return mask_to_ndarray(mask);
3654+
}, py::arg("coords"), py::arg("epsilon") = 0.0, py::arg("recursive") = true);
3655+
3656+
// rdp_mask from list<list<int>>
3657+
m.def("rdp_mask", [](std::vector<std::vector<int_>> coords, double epsilon, bool recursive) -> std::unique_ptr<ndarray_base> {
3658+
int input_cols;
3659+
auto eigen_coords = vec2d_to_eigen_i(coords, input_cols);
3660+
auto mask = rdp::douglas_simplify_mask(eigen_coords, epsilon, recursive);
3661+
return mask_to_ndarray(mask);
3662+
}, py::arg("coords"), py::arg("epsilon") = 0.0, py::arg("recursive") = true);
3663+
3664+
// rdp_mask from ndarray
3665+
m.def("rdp_mask", [](const ndarray_base& coords, double epsilon, bool recursive) -> std::unique_ptr<ndarray_base> {
3666+
int input_cols;
3667+
auto eigen_coords = ndarray_to_eigen(coords, input_cols);
3668+
auto mask = rdp::douglas_simplify_mask(eigen_coords, epsilon, recursive);
3669+
return mask_to_ndarray(mask);
3670+
}, py::arg("coords"), py::arg("epsilon") = 0.0, py::arg("recursive") = true);
3671+
3672+
// Create pocket_numpy module that re-exports rdp/rdp_mask from numpy
3673+
{
3674+
py_GlobalRef pocket_numpy_mod = py_newmodule("pocket_numpy");
3675+
if (py_getattr(m.ptr(), py_name("rdp"))) {
3676+
py_setattr(pocket_numpy_mod, py_name("rdp"), py_retval());
3677+
}
3678+
if (py_getattr(m.ptr(), py_name("rdp_mask"))) {
3679+
py_setattr(pocket_numpy_mod, py_name("rdp_mask"), py_retval());
3680+
}
3681+
}
35383682
}

tests/test_rdp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
ret = rdp(arr, epsilon=1.0 - 1e-6)
1212
assert ret.tolist() == arr
1313

14-
arr = [[*xy, 0.0] for xy in arr]
14+
arr = [[xy[0], xy[1], 0.0] for xy in arr]
1515
ret = rdp(arr, epsilon=1.0)
1616
assert ret.tolist() == [[0, 0, 0], [10, 0, 0]]
1717
ret = rdp(arr, epsilon=1.0 - 1e-6)

0 commit comments

Comments
 (0)