Skip to content

Commit a8823a9

Browse files
authored
Merge pull request #16 from 3DFin/develop_v0_3_0
v0.3.0
2 parents 98f4a7e + 58e5d27 commit a8823a9

File tree

11 files changed

+106
-50
lines changed

11 files changed

+106
-50
lines changed

.github/workflows/wheels.yml

+11-11
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ${{ matrix.os }}
1616
strategy:
1717
matrix:
18-
os: [ubuntu-22.04, windows-2022, macos-14]
18+
os: [ubuntu-latest, windows-latest, macos-13, macos-latest]
1919
fail-fast: false
2020

2121
steps:
@@ -27,14 +27,14 @@ jobs:
2727
- uses: actions/setup-python@v5
2828

2929
- name: Install cibuildwheel
30-
run: python -m pip install cibuildwheel==2.15
30+
run: python -m pip install cibuildwheel==2.20.0
3131

3232
- name: Build wheels
3333
run: python -m cibuildwheel --output-dir wheelhouse
3434

35-
- uses: actions/upload-artifact@v3
35+
- uses: actions/upload-artifact@v4
3636
with:
37-
name: wheels
37+
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
3838
path: ./wheelhouse/*.whl
3939

4040
publish:
@@ -48,20 +48,20 @@ jobs:
4848
with:
4949
submodules: True
5050

51-
- uses: actions/setup-python@v4
51+
- uses: actions/setup-python@v5
5252

53-
- uses: actions/download-artifact@v3
53+
- uses: actions/download-artifact@v4
5454
with:
55-
name: wheels
56-
path: wheelhouse
55+
pattern: cibw-*
56+
path: dist
57+
merge-multiple: true
5758

5859
- name: Publish to PyPI
5960
env:
6061
TWINE_USERNAME: __token__
6162
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
6263
# construct the source package and upload src and wheels to PiPy
6364
run: |
64-
python -m pip install twine
65-
python setup.py sdist
65+
python -m pip install twine build --upgrade
66+
python -m build --sdist
6667
twine upload dist/*
67-
twine upload wheelhouse/*.whl

CMakeLists.txt

+10-9
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,19 @@ find_package(nanobind CONFIG REQUIRED)
1616

1717
nanobind_add_module(pgeof_ext NOMINSIZE STABLE_ABI LTO src/pgeof_ext.cpp)
1818

19-
# for nanobind 2
20-
# nanobind_add_stub(
21-
# pgeof_ext_stub
22-
# MODULE pgeof_ext
23-
# OUTPUT pgeof_ext.pyi
24-
# PYTHON_PATH $<TARGET_FILE_DIR:pgeof_ext>
25-
# DEPENDS pgeof_ext
26-
# )
19+
nanobind_add_stub(
20+
pgeof_ext_stub
21+
MODULE pgeof_ext
22+
OUTPUT pgeof_ext.pyi
23+
MARKER_FILE py.typed
24+
PYTHON_PATH $<TARGET_FILE_DIR:pgeof_ext>
25+
DEPENDS pgeof_ext
26+
)
2727

2828
# All lib are headeer only.
2929
# it's faster to include like this than using exported targets
3030
# (i.e add_subdirectories(...))
3131
target_include_directories(pgeof_ext PRIVATE "include" "third_party/eigen" "third_party/nanoflann/include" "third_party/taskflow")
3232

33-
install(TARGETS pgeof_ext LIBRARY DESTINATION pgeof)
33+
install(TARGETS pgeof_ext LIBRARY DESTINATION pgeof)
34+
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pgeof_ext.pyi ${CMAKE_CURRENT_BINARY_DIR}/py.typed DESTINATION pgeof)

include/nn_search.hpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ static std::pair<nb::ndarray<nb::numpy, uint32_t, nb::ndim<2>>, nb::ndarray<nb::
3636

3737
if (knn > data.rows()) { throw std::invalid_argument("knn size is greater than the data point cloud size"); }
3838

39-
kd_tree_t kd_tree(3, data, 10);
39+
kd_tree_t kd_tree(3, data, 10, 0);
4040
const Eigen::Index n_points = query.rows();
4141
uint32_t* indices = new uint32_t[knn * n_points];
4242
nb::capsule owner_indices(indices, [](void* p) noexcept { delete[] (uint32_t*)p; });
@@ -94,7 +94,7 @@ static std::pair<nb::ndarray<nb::numpy, int32_t, nb::ndim<2>>, nb::ndarray<nb::n
9494
throw std::invalid_argument("max knn size is greater than the data point cloud size");
9595
}
9696

97-
kd_tree_t kd_tree(3, data, 10);
97+
kd_tree_t kd_tree(3, data, 10, 0);
9898
const real_t sq_search_radius = search_radius * search_radius;
9999

100100
const Eigen::Index n_points = query.rows();

include/pgeof.hpp

+22-15
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ static inline void flush() { std::cout << std::endl; };
7373
* @return the geometric features associated with each point's neighborhood in a (num_points, features_count) nd::array.
7474
*/
7575
template <typename real_t = float, const size_t feature_count = 11>
76-
static nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, feature_count>> compute_geometric_features(
77-
RefCloud<real_t> xyz, nb::ndarray<const uint32_t, nb::ndim<1>> nn, nb::ndarray<const uint32_t, nb::ndim<1>> nn_ptr,
78-
const size_t k_min, const bool verbose)
76+
static nb::ndarray<nb::numpy, real_t, nb::shape<-1, static_cast<nb::ssize_t>(feature_count)>>
77+
compute_geometric_features(
78+
RefCloud<real_t> xyz, nb::ndarray<const uint32_t, nb::ndim<1>> nn,
79+
nb::ndarray<const uint32_t, nb::ndim<1>> nn_ptr, const size_t k_min, const bool verbose)
7980
{
8081
if (k_min < 1) { throw std::invalid_argument("k_min should be > 1"); }
8182
// Each point can be treated in parallel
@@ -111,7 +112,8 @@ static nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, feature_count>> compute
111112
// Final print to start on a new line
112113
if (verbose) log::flush();
113114
const size_t shape[2] = {n_points, feature_count};
114-
return nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, feature_count>>(features, 2, shape, owner_features);
115+
return nb::ndarray<nb::numpy, real_t, nb::shape<-1, static_cast<nb::ssize_t>(feature_count)>>(
116+
features, 2, shape, owner_features);
115117
}
116118
/**
117119
* Convenience function that check that scales are well ordered in increasing order.
@@ -155,9 +157,10 @@ static bool check_scales(const std::vector<uint32_t>& k_scales)
155157
* nd::array
156158
*/
157159
template <typename real_t, const size_t feature_count = 11>
158-
static nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, nb::any, feature_count>> compute_geometric_features_multiscale(
159-
RefCloud<real_t> xyz, nb::ndarray<const uint32_t, nb::ndim<1>> nn, nb::ndarray<const uint32_t, nb::ndim<1>> nn_ptr,
160-
const std::vector<uint32_t>& k_scales, const bool verbose)
160+
static nb::ndarray<nb::numpy, real_t, nb::shape<-1, -1, static_cast<nb::ssize_t>(feature_count)>>
161+
compute_geometric_features_multiscale(
162+
RefCloud<real_t> xyz, nb::ndarray<const uint32_t, nb::ndim<1>> nn,
163+
nb::ndarray<const uint32_t, nb::ndim<1>> nn_ptr, const std::vector<uint32_t>& k_scales, const bool verbose)
161164
{
162165
if (!check_scales(k_scales))
163166
{
@@ -203,7 +206,7 @@ static nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, nb::any, feature_count>
203206
if (verbose) log::flush();
204207

205208
const size_t shape[3] = {n_points, n_scales, feature_count};
206-
return nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, nb::any, feature_count>>(
209+
return nb::ndarray<nb::numpy, real_t, nb::shape<-1, -1, static_cast<nb::ssize_t>(feature_count)>>(
207210
features, 3, shape, owner_features);
208211
}
209212

@@ -238,9 +241,11 @@ static nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, nb::any, feature_count>
238241
* @return Geometric features associated with each point's neighborhood in a (num_points, features_count) nd::array
239242
*/
240243
template <typename real_t, const size_t feature_count = 12>
241-
static nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, feature_count>> compute_geometric_features_optimal(
242-
RefCloud<real_t> xyz, nb::ndarray<const uint32_t, nb::ndim<1>> nn, nb::ndarray<const uint32_t, nb::ndim<1>> nn_ptr,
243-
const uint32_t k_min, const uint32_t k_step, const uint32_t k_min_search, const bool verbose)
244+
static nb::ndarray<nb::numpy, real_t, nb::shape<-1, static_cast<nb::ssize_t>(feature_count)>>
245+
compute_geometric_features_optimal(
246+
RefCloud<real_t> xyz, nb::ndarray<const uint32_t, nb::ndim<1>> nn,
247+
nb::ndarray<const uint32_t, nb::ndim<1>> nn_ptr, const uint32_t k_min, const uint32_t k_step,
248+
const uint32_t k_min_search, const bool verbose)
244249
{
245250
if (k_min < 1 && k_min_search < 1) { throw std::invalid_argument("k_min and k_min_search should be > 1"); }
246251
// Each point can be treated in parallel
@@ -300,7 +305,8 @@ static nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, feature_count>> compute
300305
if (verbose) log::flush();
301306

302307
const size_t shape[2] = {n_points, feature_count};
303-
return nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, feature_count>>(features, 2, shape, owner_features);
308+
return nb::ndarray<nb::numpy, real_t, nb::shape<-1, static_cast<nb::ssize_t>(feature_count)>>(
309+
features, 2, shape, owner_features);
304310
}
305311

306312
/**
@@ -317,14 +323,14 @@ static nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, feature_count>> compute
317323
* @return Geometric features associated with each point's neighborhood in a (num_points, features_count) nd::array
318324
*/
319325
template <typename real_t>
320-
static nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, nb::any>> compute_geometric_features_selected(
326+
static nb::ndarray<nb::numpy, real_t, nb::shape<-1, -1>> compute_geometric_features_selected(
321327
RefCloud<real_t> xyz, const real_t search_radius, const uint32_t max_knn,
322328
const std::vector<EFeatureID>& selected_features)
323329
{
324330
using kd_tree_t = nanoflann::KDTreeEigenMatrixAdaptor<RefCloud<real_t>, 3, nanoflann::metric_L2_Simple>;
325331
// TODO: where knn < num of points
326332

327-
kd_tree_t kd_tree(3, xyz, 10);
333+
kd_tree_t kd_tree(3, xyz, 10, 0);
328334
const size_t feature_count = selected_features.size();
329335
const Eigen::Index n_points = xyz.rows();
330336
real_t sq_search_radius = search_radius * search_radius;
@@ -364,6 +370,7 @@ static nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, nb::any>> compute_geome
364370
});
365371
executor.run(taskflow).get();
366372

367-
return nb::ndarray<nb::numpy, real_t, nb::shape<nb::any, nb::any>>(features, {static_cast<size_t>(n_points), feature_count}, owner_features);
373+
return nb::ndarray<nb::numpy, real_t, nb::shape<-1, -1>>(
374+
features, {static_cast<size_t>(n_points), feature_count}, owner_features);
368375
}
369376
} // namespace pgeof

pyproject.toml

+47-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
[build-system]
2-
requires = ["scikit-build-core >=0.4.3", "nanobind == 1.9.2"]
2+
requires = ["scikit-build-core >=0.4.3",
3+
"nanobind == 2.1.0",
4+
"typing_extensions;python_version < '3.11'"
5+
]
36
build-backend = "scikit_build_core.build"
47

58
[project]
69
name = "pgeof"
7-
version = "0.2.0"
10+
version = "0.3.0"
811
readme = "README.md"
912
description = "Compute the geometric features associated with each point's neighborhood:"
1013
requires-python = ">=3.8,<3.13"
@@ -36,6 +39,47 @@ build-dir = "build/{wheel_tag}"
3639

3740
cmake.build-type = "Release"
3841

42+
# make sdist a lot lighter by removing some useless files from third_party
43+
# ⚠️ be sure to keep copyrights and license file
44+
sdist.exclude = [
45+
"third_party/eigen/bench",
46+
"third_party/eigen/demos",
47+
"third_party/eigen/doc",
48+
"third_party/taskflow/3rd-party",
49+
"third_party/taskflow/benchmarks",
50+
"third_party/taskflow/docs",
51+
"third_party/taskflow/doxygen",
52+
"third_party/taskflow/examples",
53+
"third_party/taskflow/sandbox",
54+
"third_party/taskflow/unittests",
55+
]
56+
57+
[tool.ruff]
58+
target-version = "py310"
59+
line-length = 120
60+
61+
[tool.ruff.lint]
62+
# TODO Add D, PTH, RET, disabled for now as they collides with intial choices
63+
select = ["E", "W", "YTT", "NPY", "PYI", "Q", "F", "B", "I", "SIM", "RUF"]
64+
# TODO: for now we ignore "Line too long error (E501)"
65+
# because our comments are too longs
66+
# code formatting will take care of the line length in code anyway
67+
ignore = [
68+
"E501",
69+
# Ignore docstring in public package and module
70+
"D100",
71+
"D104",
72+
# Blank line before class
73+
"D203",
74+
# multiline summary second line
75+
"D213",
76+
# yoda conditions
77+
"SIM300",
78+
]
79+
80+
[tool.ruff.lint.isort]
81+
known-first-party = ["pgeof"]
82+
3983
[tool.tox]
4084
legacy_tox_ini = """
4185
[tox]
@@ -67,4 +111,4 @@ archs = ["auto64"] # limit to 64bits builds
67111

68112
# Needed for full C++17 support
69113
[tool.cibuildwheel.macos.environment]
70-
MACOSX_DEPLOYMENT_TARGET = "10.14"
114+
MACOSX_DEPLOYMENT_TARGET = "11.0"

src/pgeof_ext.cpp

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11

22
#include <nanobind/nanobind.h>
33
#include <nanobind/ndarray.h>
4-
#include <nanobind/stl/pair.h>
54
#include <nanobind/stl/vector.h>
65

76
#include "nn_search.hpp"
@@ -176,4 +175,4 @@ NB_MODULE(pgeof_ext, m)
176175
:param selected_features: List of selected features. See EFeatureID
177176
:return: Geometric features associated with each point's neighborhood in a (num_points, features_count) numpy array.
178177
)");
179-
}
178+
}

tests/bench_jakteristics.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111

1212
@pytest.fixture
1313
def random_point_cloud():
14-
return np.random.rand(1000000, 3) * 100
14+
rng = np.random.default_rng()
15+
return rng.random(0, 100, size=((1000000, 3)))
1516

1617

1718
@pytest.mark.benchmark(group="feature-computation-jak", disable_gc=True, warmup=True)

tests/bench_knn.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
@pytest.fixture
99
def random_point_cloud():
10-
return np.random.rand(100000, 3).astype("float32")
10+
rng = np.random.default_rng()
11+
return rng.random(0, 100, size=((1000000, 3))).astype("float32")
1112

1213

1314
@pytest.mark.benchmark(group="knn", disable_gc=True, warmup=True)

tests/helpers.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
def random_nn(num_points, k):
66
# Generate a random synthetic point cloud
7-
xyz = np.random.rand(num_points, 3)
7+
rng = np.random.default_rng()
8+
xyz = rng.random(size=(num_points, 3))
89

910
# Converting k-nearest neighbors to CSR format
1011
kneigh = KDTree(xyz).query(xyz, k=k, workers=-1)
@@ -20,4 +21,4 @@ def random_nn(num_points, k):
2021
xyz = np.ascontiguousarray(xyz)
2122
nn_ptr = np.ascontiguousarray(nn_ptr)
2223
nn = np.ascontiguousarray(nn)
23-
return xyz, nn, nn_ptr
24+
return xyz, nn, nn_ptr

tests/test_pgeof.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
def test_knn():
99
knn = 10
10-
xyz = np.random.rand(1000, 3)
10+
rng = np.random.default_rng()
11+
xyz = rng.random(size=(1000, 3))
1112
xyz = xyz.astype("float32")
1213
tree = KDTree(xyz)
1314
_, k_legacy = tree.query(xyz, k=knn, workers=-1)
@@ -18,7 +19,8 @@ def test_knn():
1819
def test_radius_search():
1920
knn = 10
2021
radius = 0.2
21-
xyz = np.random.rand(1000, 3)
22+
rng = np.random.default_rng()
23+
xyz = rng.random(size=(1000, 3))
2224
xyz = xyz.astype("float32")
2325
tree = KDTree(xyz)
2426
_, k_legacy = tree.query(xyz, k=knn, distance_upper_bound=radius, workers=-1)
@@ -43,4 +45,4 @@ def test_pgeof_multiscale():
4345
simple = pgeof.compute_features(xyz, nn, nn_ptr, 50, False)
4446
multi_simple = pgeof.compute_features_multiscale(xyz, nn, nn_ptr, [20], False)
4547
np.testing.assert_allclose(multi[:, 0], multi_simple[:, 0], 1e-1, 1e-5)
46-
np.testing.assert_allclose(multi[:, 1], simple, 1e-1, 1e-5)
48+
np.testing.assert_allclose(multi[:, 1], simple, 1e-1, 1e-5)

0 commit comments

Comments
 (0)