Skip to content

Commit

Permalink
fix python binding (#987)
Browse files Browse the repository at this point in the history
* python: fix type casters and warp batch

* python: allow docstring override

* update nanobind version

* python: add docstring_override.txt

* python: fix example

* version limit for nanobind
  • Loading branch information
pca006132 authored Oct 18, 2024
1 parent 005102c commit e718288
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 35 deletions.
5 changes: 4 additions & 1 deletion bindings/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ else()
nanobind
GIT_REPOSITORY https://github.com/wjakob/nanobind.git
GIT_TAG
9641bb7151f04120013b812789b3ebdfa7e7324f # v2.1.0
784efa2a0358a4dc5432c74f5685ee026e20f2b6 # v2.2.0
GIT_PROGRESS TRUE
)
FetchContent_MakeAvailable(nanobind)
set(NB_VERSION 2.2.0)
endif()

if(NB_VERSION VERSION_LESS 2.1.0)
Expand Down Expand Up @@ -96,6 +97,8 @@ set(
${CMAKE_SOURCE_DIR}/src/cross_section/cross_section.cpp
${CMAKE_SOURCE_DIR}/src/polygon.cpp
${CMAKE_SOURCE_DIR}/include/manifold/common.h
${CMAKE_CURRENT_SOURCE_DIR}/gen_docs.py
${CMAKE_CURRENT_SOURCE_DIR}/docstring_override.txt
)
add_custom_command(
OUTPUT autogen_docstrings.inl
Expand Down
51 changes: 51 additions & 0 deletions bindings/python/docstring_override.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
cross_section__warp__warp_func:
Move the vertices of this CrossSection (creating a new one) according to
any arbitrary input function, followed by a union operation (with a
Positive fill rule) that ensures any introduced intersections are not
included in the result.
:param warp_func: A function that takes the original vertex position and
return the new position.
cross_section__warp_batch__warp_func:
Same as CrossSection::warp but calls warpFunc with
an ndarray[n, 2] instead of processing only one vertex at a time.
:param warp_func: A function that takes multiple vertex positions as an
ndarray[n, 2] and returns the new vertex positions.
manifold__warp__warp_func:
This function does not change the topology, but allows the vertices to be
moved according to any arbitrary input function. It is easy to create a
function that warps a geometrically valid object into one which overlaps, but
that is not checked here, so it is up to the user to choose their function
with discretion.
:param warp_func: A function that takes the original vertex position and
return the new position.
manifold__warp_batch__warp_func:
Same as Manifold::warp but calls warpFunc with with
an ndarray[n, 3] instead of processing only one vertex at a time.
:param warp_func: A function that takes multiple vertex positions as an
ndarray[n, 3] and returns the new vertex positions. The result should have the
same shape as the input.
manifold__smooth__mesh_gl__sharpened_edges:
Constructs a smooth version of the input mesh by creating tangents; this
method will throw if you have supplied tangents with your mesh already. The
actual triangle resolution is unchanged; use the Refine() method to
interpolate to a higher-resolution curve.
By default, every edge is calculated for maximum smoothness (very much
approximately), attempting to minimize the maximum mean Curvature magnitude.
No higher-order derivatives are considered, as the interpolation is
independent per triangle, only sharing constraints on their boundaries.
:param mesh: input Mesh.
:param sharpened_edges: If desired, you can supply a vector of sharpened
halfedges, which should in general be a small subset of all halfedges. Order
of entries doesn't matter, as each one specifies the desired smoothness
(between zero and one, with one the default for all unspecified halfedges)
and the halfedge index (3 * triangle index + [0,1,2] where 0 is the edge
between triVert 0 and 1, etc).
:param edge_smoothness: Smoothness values associated to each halfedge defined
in sharpened_edges. At a smoothness value of zero, a sharp crease is made. The
smoothness is interpolated along each edge, so the specified value should be
thought of as an average. Where exactly two sharpened edges meet at a vertex,
their tangents are rotated to be colinear so that the sharpened edge can be
continuous. Vertices with only one sharpened edge are completely smooth,
allowing sharpened edges to smoothly vanish at termination. A single vertex
can be sharpened by sharping all edges that are incident on it, allowing cones
to be formed.
2 changes: 1 addition & 1 deletion bindings/python/examples/all_apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def all_manifold():
m = m.translate((0, 0, 0))
m = m.trim_by_plane((0, 0, 1), 0)
m = m.warp(lambda p: (p[0] + 1, p[1] / 2, p[2] * 2))
m = m.warp_batch(lambda ps: ps * [1, 0.5, 2] + [1, 0, 0])
m = m.warp_batch(lambda ps: ps * 2 + [1, 0, 0])
m = Manifold.cube()
m2 = Manifold.cube().translate([2, 0, 0])
d = m.min_gap(m2, 2)
Expand Down
5 changes: 2 additions & 3 deletions bindings/python/examples/test_torus_knot.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,11 @@ def func(pts):
m3 = ax_rotate(2, psi)

v = v[:, None, :] @ m1 @ m2 @ m3
pts[:] = v[:, 0, :3]
return v[:, 0, :3]

def func_single(v):
pts = np.array([v])
func(pts)
return pts[0]
return func(pts)[0]

if warp_single:
return Manifold.revolve(circle, int(m)).warp(func_single)
Expand Down
12 changes: 12 additions & 0 deletions bindings/python/gen_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ def select_functions(s):

comments = dict(sorted(comments.items()))

with open(f"{base}/bindings/python/docstring_override.txt") as f:
key = ""
for l in f:
if l.startswith(" "):
comments[key] += l[2:]
else:
key = l[:-2]
if key not in comments.keys():
print(f"Error, unknown docstring override key {key}")
exit(-1)
comments[key] = ""

gen_h = "autogen_docstrings.inl"
with open(gen_h, "w") as f:
f.write("#pragma once\n\n")
Expand Down
76 changes: 47 additions & 29 deletions bindings/python/manifold3d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,16 @@ struct nb::detail::type_caster<la::mat<T, R, C>> {
}
static handle from_cpp(la_type mat, rv_policy policy,
cleanup_list *cleanup) noexcept {
T *buffer = new T[R * C];
nb::capsule mem_mgr(buffer, [](void *p) noexcept { delete[] (T *)p; });
std::array<T, R * C> buffer;
for (int i = 0; i < R; i++) {
for (int j = 0; j < C; j++) {
// py is (Rows, Cols), la is (Cols, Rows)
buffer[i * C + j] = mat[j][i];
}
}
numpy_type arr{buffer, {R, C}, std::move(mem_mgr)};
return ndarray_wrap(arr.handle(), int(ndarray_framework::numpy), policy,
cleanup);
numpy_type arr{buffer, {R, C}, nb::handle()};
// we must copy the underlying data
return make_caster<numpy_type>::from_cpp(arr, rv_policy::copy, cleanup);
}
};

Expand Down Expand Up @@ -160,41 +159,30 @@ struct nb::detail::type_caster<std::vector<la::vec<T, N>>> {
}
}
numpy_type arr{buffer, {num_vec, N}, std::move(mem_mgr)};
return ndarray_wrap(arr.handle(), ndarray_framework::numpy, policy,
cleanup);
// we can just do a move because we already did the copying
return make_caster<numpy_type>::from_cpp(arr, rv_policy::move, cleanup);
}
};

// handle VecView<la::vec*>
// handle VecView<la::vecN>
template <class T, int N>
struct nb::detail::type_caster<manifold::VecView<la::vec<T, N>>> {
using la_type = la::vec<T, N>;
using numpy_type = nb::ndarray<nb::numpy, T, nb::shape<-1, N>>;
NB_TYPE_CASTER(manifold::VecView<la_type>,
const_name(la_name<la_type>::multi_name));

bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept {
make_caster<numpy_type> arr_cast;
if (!arr_cast.from_python(src, flags, cleanup)) return false;
// TODO try 2d iterators if numpy cast fails
size_t num_vec = arr_cast.value.shape(0);
if (num_vec != value.size()) return false;
for (size_t i = 0; i < num_vec; i++) {
for (int j = 0; j < N; j++) {
value[i][j] = arr_cast.value(i, j);
}
}
return true;
}
static handle from_cpp(Value vec, rv_policy policy,
cleanup_list *cleanup) noexcept {
// do we have ownership issue here?
size_t num_vec = vec.size();
static_assert(sizeof(vec[0]) == (N * sizeof(T)),
// assume packed struct
static_assert(alignof(la::vec<T, N>) <= (N * sizeof(T)),
"VecView -> numpy requires packed structs");
numpy_type arr{&vec[0], {num_vec, N}, nb::handle()};
return ndarray_wrap(arr.handle(), ndarray_framework::numpy, policy,
cleanup);
static_assert(sizeof(la::vec<T, N>) == (N * sizeof(T)),
"VecView -> numpy requires packed structs");
numpy_type arr{vec.data(), {num_vec, N}, nb::handle()};
// we must copy the underlying data
return make_caster<numpy_type>::from_cpp(arr, rv_policy::copy, cleanup);
}
};

Expand Down Expand Up @@ -274,8 +262,22 @@ NB_MODULE(manifold3d, m) {
return self.Warp([&warp_func](vec3 &v) { v = warp_func(v); });
},
nb::arg("warp_func"), manifold__warp__warp_func)
.def("warp_batch", &Manifold::WarpBatch, nb::arg("warp_func"),
manifold__warp_batch__warp_func)
.def(
"warp_batch",
[](const Manifold &self,
std::function<nb::object(VecView<vec3>)> warp_func) {
// need a wrapper because python cant modify a reference in-place
return self.WarpBatch([&warp_func](VecView<vec3> v) {
auto tmp = warp_func(v);
nb::ndarray<double, nb::shape<-1, 3>, nanobind::c_contig> tmpnd;
if (!nb::try_cast(tmp, tmpnd) || tmpnd.ndim() != 2)
throw std::runtime_error(
"Invalid vector shape, expected (:, 3)");
std::copy(tmpnd.data(), tmpnd.data() + v.size() * 3,
&v.data()->x);
});
},
nb::arg("warp_func"), manifold__warp_batch__warp_func)
.def(
"set_properties",
[](const Manifold &self, int newNumProp,
Expand Down Expand Up @@ -400,7 +402,6 @@ NB_MODULE(manifold3d, m) {
},
nb::arg("mesh"), nb::arg("sharpened_edges") = nb::list(),
nb::arg("edge_smoothness") = nb::list(),
// TODO: params slightly diff
manifold__smooth__mesh_gl__sharpened_edges)
.def_static("batch_boolean", &Manifold::BatchBoolean,
nb::arg("manifolds"), nb::arg("op"),
Expand Down Expand Up @@ -669,6 +670,23 @@ NB_MODULE(manifold3d, m) {
nb::arg("warp_func"), cross_section__warp__warp_func)
.def("warp_batch", &CrossSection::WarpBatch, nb::arg("warp_func"),
cross_section__warp_batch__warp_func)

.def(
"warp_batch",
[](const CrossSection &self,
std::function<nb::object(VecView<vec2>)> warp_func) {
// need a wrapper because python cant modify a reference in-place
return self.WarpBatch([&warp_func](VecView<vec2> v) {
auto tmp = warp_func(v);
nb::ndarray<double, nb::shape<-1, 2>, nanobind::c_contig> tmpnd;
if (!nb::try_cast(tmp, tmpnd) || tmpnd.ndim() != 2)
throw std::runtime_error(
"Invalid vector shape, expected (:, 2)");
std::copy(tmpnd.data(), tmpnd.data() + v.size() * 2,
&v.data()->x);
});
},
nb::arg("warp_func"), cross_section__warp_batch__warp_func)
.def("simplify", &CrossSection::Simplify, nb::arg("epsilon") = 1e-6,
cross_section__simplify__epsilon)
.def(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dependencies = [

[build-system]
requires = [
"nanobind>=1.8.0",
"nanobind>=1.8.0,<=2.2.0",
"scikit-build-core",
]
build-backend = "scikit_build_core.build"
Expand Down

0 comments on commit e718288

Please sign in to comment.