Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Immutable dataset views for readonly morphology classes #373

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
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
// sire. The parent that holds that returned data should be passed as a base. See:
eleftherioszisis marked this conversation as resolved.
Show resolved Hide resolved
// 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 unerlying data. See limitations:
eleftherioszisis marked this conversation as resolved.
Show resolved Hide resolved
// 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