Skip to content
This repository was archived by the owner on Feb 26, 2025. It is now read-only.
41 changes: 20 additions & 21 deletions binds/python/bind_immutable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@ void bind_immutable_module(py::module& m) {
// Property accessors
.def_property_readonly(
"points",
[](morphio::Morphology* morpho) {
const auto& data = morpho->points();
return py::array(static_cast<py::ssize_t>(data.size()), data.data());
[](const morphio::Morphology& morpho) {
return internal_vector_as_readonly_array(morpho.points(), morpho);
},
"Returns a list with all points from all sections (soma points are not included)\n"
"Note: points belonging to the n'th section are located at indices:\n"
Expand All @@ -96,17 +95,15 @@ void bind_immutable_module(py::module& m) {
.def_property_readonly(
"diameters",
[](const morphio::Morphology& morpho) {
const auto& data = morpho.diameters();
return py::array(static_cast<py::ssize_t>(data.size()), data.data());
return internal_vector_as_readonly_array(morpho.diameters(), morpho);
},
"Returns a list with all diameters from all sections (soma points are not included)\n"
"Note: diameters belonging to the n'th section are located at indices:\n"
"[Morphology.sectionOffsets(n), Morphology.sectionOffsets(n+1)[")
.def_property_readonly(
"perimeters",
[](const morphio::Morphology& obj) {
const auto& data = obj.perimeters();
return py::array(static_cast<py::ssize_t>(data.size()), data.data());
[](const morphio::Morphology& morpho) {
return internal_vector_as_readonly_array(morpho.perimeters(), morpho);
},
"Returns a list with all perimeters from all sections (soma points are not included)\n"
"Note: perimeters belonging to the n'th section are located at indices:\n"
Expand All @@ -125,8 +122,7 @@ void bind_immutable_module(py::module& m) {
.def_property_readonly(
"section_types",
[](const morphio::Morphology& morph) {
const auto& data = morph.sectionTypes();
return py::array(static_cast<py::ssize_t>(data.size()), data.data());
return internal_vector_as_readonly_array(morph.sectionTypes(), morph);
},
"Returns a vector with the section type of every section")
.def_property_readonly("connectivity",
Expand Down Expand Up @@ -268,7 +264,9 @@ void bind_immutable_module(py::module& m) {
"(dendrite, axon, ...)")
.def_property_readonly(
"points",
[](morphio::Section* section) { return span_array_to_ndarray(section->points()); },
[](const morphio::Section& self) {
return internal_vector_as_readonly_array(self.points(), self);
},
"Returns list of section's point coordinates")

.def_property_readonly(
Expand All @@ -278,11 +276,15 @@ void bind_immutable_module(py::module& m) {

.def_property_readonly(
"diameters",
[](morphio::Section* section) { return span_to_ndarray(section->diameters()); },
[](const morphio::Section& self) {
return internal_vector_as_readonly_array(self.diameters(), self);
},
"Returns list of section's point diameters")
.def_property_readonly(
"perimeters",
[](morphio::Section* section) { return span_to_ndarray(section->perimeters()); },
[](const morphio::Section& self) {
return internal_vector_as_readonly_array(self.perimeters(), self);
},
"Returns list of section's point perimeters")

.def("is_heterogeneous",
Expand Down Expand Up @@ -411,18 +413,16 @@ void bind_immutable_module(py::module& m) {
// Property accessors
.def_property_readonly(
"points",
[](morphio::DendriticSpine* morpho) {
const auto& data = morpho->points();
return py::array(static_cast<py::ssize_t>(data.size()), data.data());
[](const morphio::DendriticSpine& morph) {
return internal_vector_as_readonly_array(morph.points(), morph);
},
"Returns a list with all points from all sections\n"
"Note: points belonging to the n'th section are located at indices:\n"
"[DendriticSpine.sectionOffsets(n), DendriticSpine.sectionOffsets(n+1)[")
.def_property_readonly(
"diameters",
[](const morphio::DendriticSpine& morpho) {
const auto& data = morpho.diameters();
return py::array(static_cast<py::ssize_t>(data.size()), data.data());
[](const morphio::DendriticSpine& morph) {
return internal_vector_as_readonly_array(morph.diameters(), morph);
},
"Returns a list with all diameters from all sections\n"
"Note: diameters belonging to the n'th section are located at indices:\n"
Expand All @@ -443,8 +443,7 @@ void bind_immutable_module(py::module& m) {
.def_property_readonly(
"section_types",
[](const morphio::DendriticSpine& morph) {
const auto& data = morph.sectionTypes();
return py::array(static_cast<py::ssize_t>(data.size()), data.data());
return internal_vector_as_readonly_array(morph.diameters(), morph);
},
"Returns a vector with the section type of every section")
.def_property_readonly("connectivity",
Expand Down
28 changes: 12 additions & 16 deletions binds/python/bind_vasculature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,8 @@ void bind_vasculature(py::module& m) {
// Property accessors
.def_property_readonly(
"points",
[](morphio::vasculature::Vasculature* morpho) {
return py::array(static_cast<py::ssize_t>(morpho->points().size()),
morpho->points().data());
[](const morphio::vasculature::Vasculature& self) {
return internal_vector_as_readonly_array(self.points(), self);
},
"Returns a list with all points from all sections")

Expand All @@ -67,24 +66,21 @@ void bind_vasculature(py::module& m) {

.def_property_readonly(
"diameters",
[](morphio::vasculature::Vasculature* morpho) {
auto diameters = morpho->diameters();
return py::array(static_cast<py::ssize_t>(diameters.size()), diameters.data());
[](const morphio::vasculature::Vasculature& self) {
return internal_vector_as_readonly_array(self.diameters(), self);
},
"Returns a list with all diameters from all sections")
.def_property_readonly(
"section_types",
[](morphio::vasculature::Vasculature* obj) {
auto data = obj->sectionTypes();
return py::array(static_cast<py::ssize_t>(data.size()), data.data());
[](const morphio::vasculature::Vasculature& self) {
return internal_vector_as_readonly_array(self.sectionTypes(), self);
},
"Returns a vector with the section type of every section")

.def_property_readonly(
"section_connectivity",
[](morphio::vasculature::Vasculature* morpho) {
return py::array(static_cast<py::ssize_t>(morpho->sectionConnectivity().size()),
morpho->sectionConnectivity().data());
[](const morphio::vasculature::Vasculature& self) {
return internal_vector_as_readonly_array(self.sectionConnectivity(), self);
},
"Returns a 2D array of the section connectivity")

Expand Down Expand Up @@ -128,8 +124,8 @@ void bind_vasculature(py::module& m) {
"Returns the morphological type of this section")
.def_property_readonly(
"points",
[](morphio::vasculature::Section* section) {
return span_array_to_ndarray(section->points());
[](const morphio::vasculature::Section& self) {
return internal_vector_as_readonly_array(self.points(), self);
},
"Returns list of section's point coordinates")

Expand All @@ -140,8 +136,8 @@ void bind_vasculature(py::module& m) {

.def_property_readonly(
"diameters",
[](morphio::vasculature::Section* section) {
return span_to_ndarray(section->diameters());
[](const morphio::vasculature::Section& self) {
return internal_vector_as_readonly_array(self.points(), self);
},
"Returns list of section's point diameters")

Expand Down
18 changes: 18 additions & 0 deletions binds/python/bindings_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,21 @@ inline py::array_t<typename Sequence::value_type> as_pyarray(Sequence&& seq) {
capsule // numpy array references this parent
);
}

/**
* @brief Exposes an internal vector (not temporary) as a non-writeable numpy array.
*/
template <typename Sequence, typename Parent>
inline py::array internal_vector_as_readonly_array(const Sequence& seq, const Parent& parent) {
// The correct base must be used here so that refcounting is done correctly on the python
// side. The parent that holds that returned data should be passed as a base. See:
// https://github.com/pybind/pybind11/issues/2271#issuecomment-650740842
auto res = py::array(static_cast<py::ssize_t>(seq.size()), seq.data(), py::cast(parent));

// pybind11 casts away const-ness in return values. Thus, we need to explicitly set the
// numpy array flag to not writeable to prevent mutating the underlying data. See limitations:
// https://pybind11.readthedocs.io/en/stable/limitations.html
py::detail::array_proxy(res.ptr())->flags &= ~py::detail::npy_api::NPY_ARRAY_WRITEABLE_;

return res;
}
1 change: 1 addition & 0 deletions tests/test_4_immut.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def test_components():
for cell in CELLS.values():
assert cell.n_points == len(cell.points)
assert cell.section(0).n_points == len(cell.section(0).points)
assert not cell.points.flags.writeable


def test_is_root():
Expand Down
Loading