LCOV - code coverage report
Current view: top level - python - bind_slice_methods.h (source / functions) Hit Total Coverage
Test: coverage.info Lines: 174 190 91.6 %
Date: 2024-11-17 01:47:58 Functions: 102 120 85.0 %

          Line data    Source code
       1             : // SPDX-License-Identifier: BSD-3-Clause
       2             : // Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
       3             : /// @file
       4             : /// @author Simon Heybrock
       5             : #pragma once
       6             : 
       7             : #include "numpy.h"
       8             : #include "pybind11.h"
       9             : #include "scipp/core/dtype.h"
      10             : #include "scipp/core/slice.h"
      11             : #include "scipp/core/tag_util.h"
      12             : #include "scipp/dataset/dataset.h"
      13             : #include "scipp/dataset/extract.h"
      14             : #include "scipp/dataset/shape.h"
      15             : #include "scipp/dataset/slice.h"
      16             : #include "scipp/dataset/util.h"
      17             : #include "scipp/variable/slice.h"
      18             : #include "scipp/variable/variable.h"
      19             : #include "slice_utils.h"
      20             : 
      21             : namespace py = pybind11;
      22             : using namespace scipp;
      23             : using namespace scipp::variable;
      24             : using namespace scipp::dataset;
      25             : 
      26        2942 : template <class T> auto to_dim_type(std::tuple<std::string, T> &&t) {
      27        2942 :   return std::tuple{Dim{std::move(std::get<0>(t))}, std::move(std::get<1>(t))};
      28             : }
      29             : 
      30        5896 : template <class T> auto dim_extent(const T &object, const Dim dim) {
      31             :   if constexpr (std::is_same_v<T, Dataset>) {
      32         142 :     scipp::index extent = -1;
      33         142 :     if (object.sizes().contains(dim))
      34         142 :       extent = object.sizes().at(dim);
      35         142 :     return extent;
      36             :   } else {
      37        5754 :     return object.dims()[dim];
      38             :   }
      39             : }
      40             : 
      41             : template <class T>
      42        3205 : auto from_py_slice(const T &source,
      43             :                    const std::tuple<Dim, const py::slice> &index) {
      44        3205 :   const auto &[dim, indices] = index;
      45             :   size_t start, stop, step, slicelength;
      46        3205 :   const auto size = dim_extent(source, dim);
      47        3205 :   if (!indices.compute(size, &start, &stop, &step, &slicelength))
      48           0 :     throw py::error_already_set();
      49        3205 :   if (slicelength == 0) {
      50         749 :     stop = start; // Propagate vanishing slice length downstream.
      51             :   }
      52        6410 :   return Slice(dim, start, stop, step);
      53             : }
      54             : 
      55             : template <class View> struct SetData {
      56             :   template <class T> struct Impl {
      57         367 :     static void apply(View &slice, const py::object &obj) {
      58         367 :       if (slice.has_variances())
      59           0 :         throw std::runtime_error("Data object contains variances, to set data "
      60             :                                  "values use the `values` property or provide "
      61             :                                  "a tuple of values and variances.");
      62             : 
      63         367 :       copy_array_into_view(cast_to_array_like<T>(obj, slice.unit()),
      64             :                            slice.template values<T>(), slice.dims());
      65         367 :     }
      66             :   };
      67             : };
      68             : 
      69           8 : inline void throw_index_error(const scipp::index i, const scipp::index size) {
      70          24 :   throw std::out_of_range("The requested index " + std::to_string(i) +
      71          16 :                           " is out of range. Dimension size is " +
      72          32 :                           std::to_string(size) + " and the allowed range is [" +
      73          32 :                           std::to_string(-size) + ":" +
      74          32 :                           std::to_string(size - 1) + "].");
      75             : }
      76             : 
      77             : template <class T>
      78        2691 : auto get_slice(T &self, const std::tuple<Dim, scipp::index> &index) {
      79        2691 :   auto [dim, i] = index;
      80        2691 :   auto sz = dim_extent(self, dim);
      81        2691 :   if (i < -sz || i >= sz) // index is out of range
      82           4 :     throw_index_error(i, sz);
      83        2687 :   if (i < 0)
      84         132 :     i = sz + i;
      85        5374 :   return Slice(dim, i);
      86             : }
      87             : 
      88             : template <class T>
      89        3263 : auto get_slice_range(T &self, const std::tuple<Dim, const py::slice> &index) {
      90        3263 :   auto [dim, py_slice] = index;
      91             :   if constexpr (std::is_same_v<T, DataArray> || std::is_same_v<T, Dataset>) {
      92        2495 :     auto start = py::getattr(py_slice, "start");
      93        2495 :     auto stop = py::getattr(py_slice, "stop");
      94        2495 :     if (!start.is_none() || !stop.is_none()) { // Means default slice : is
      95             :                                                // treated as index slice
      96             :       try {
      97        2447 :         auto [start_var, stop_var] = label_bounds_from_pyslice(py_slice);
      98          57 :         return std::make_from_tuple<Slice>(
      99          57 :             get_slice_params(self, dim, start_var, stop_var));
     100        2447 :       } catch (const py::cast_error &) {
     101             :       }
     102             :     }
     103        2553 :   }
     104        3973 :   return from_py_slice(self, index);
     105        3263 : }
     106             : 
     107             : template <class T>
     108        2605 : auto getitem(T &self, const std::tuple<Dim, scipp::index> &index) {
     109        3813 :   return self.slice(get_slice(self, index));
     110             : }
     111             : 
     112             : template <class T>
     113        3198 : auto getitem(T &self, const std::tuple<Dim, py::slice> &index) {
     114        5675 :   return self.slice(get_slice_range(self, index));
     115             : }
     116             : 
     117           0 : template <class T> auto getitem(T &self, const py::ellipsis &) {
     118           0 :   return self.slice({});
     119             : }
     120             : 
     121             : // Helpers wrapped in struct to avoid unresolvable overloads.
     122             : template <class T> struct slicer {
     123         122 :   static auto get_by_value(T &self,
     124             :                            const std::tuple<std::string, Variable> &value) {
     125         122 :     auto &[dim, val] = value;
     126         122 :     return slice(self, Dim{dim}, val);
     127             :   }
     128             : 
     129         367 :   static void set_from_numpy(T &&slice, const py::object &obj) {
     130             :     core::CallDType<double, float, int64_t, int32_t,
     131         367 :                     bool>::apply<SetData<T>::template Impl>(slice.dtype(),
     132             :                                                             slice, obj);
     133         367 :   }
     134             : 
     135             :   template <class Other>
     136          86 :   static void set_from_view(T &self, const std::tuple<Dim, scipp::index> &index,
     137             :                             const Other &data) {
     138          86 :     self.setSlice(get_slice(self, index), data);
     139          74 :   }
     140             : 
     141             :   template <class Other>
     142          65 :   static void set_from_view(T &self,
     143             :                             const std::tuple<Dim, const py::slice> &index,
     144             :                             const Other &data) {
     145          65 :     self.setSlice(get_slice_range(self, index), data);
     146          63 :   }
     147             : 
     148             :   template <class Other>
     149           6 :   static void set_from_view(T &self, const py::ellipsis &, const Other &data) {
     150           6 :     self.setSlice(Slice{}, data);
     151           6 :   }
     152             : 
     153             :   template <class Other>
     154           9 :   static void set_by_value(T &self,
     155             :                            const std::tuple<std::string, Variable> &value,
     156             :                            const Other &data) {
     157           9 :     auto &[dim, val] = value;
     158           9 :     self.setSlice(
     159           9 :         std::make_from_tuple<Slice>(get_slice_params(self, Dim(dim), val)),
     160             :         data);
     161           9 :   }
     162             : 
     163             :   // Manually dispatch based on the object we are assigning from in order to
     164             :   // cast it correctly to a scipp view, numpy array or fallback std::vector.
     165             :   // This needs to happen partly based on the dtype which cannot be encoded
     166             :   // in the Python bindings directly.
     167             :   template <class IndexOrRange>
     168         524 :   static void set(T &self, const IndexOrRange &index, const py::object &data) {
     169             :     if constexpr (std::is_same_v<T, Dataset>)
     170          10 :       if (py::isinstance<Dataset>(data))
     171           8 :         return set_from_view(self, index, py::cast<Dataset>(data));
     172             : 
     173             :     if constexpr (!std::is_same_v<T, Variable>)
     174          65 :       if (py::isinstance<DataArray>(data))
     175          14 :         return set_from_view(self, index, py::cast<DataArray>(data));
     176             : 
     177         504 :     if (py::isinstance<Variable>(data))
     178         151 :       return set_from_view(self, index, py::cast<Variable>(data));
     179             : 
     180             :     if constexpr (!std::is_same_v<T, Dataset>)
     181         367 :       return set_from_numpy(getitem(self, index), data);
     182             : 
     183           0 :     std::ostringstream oss;
     184           0 :     oss << "Cannot to assign a " << py::str(data.get_type())
     185           0 :         << " to a slice of a " << py::type_id<T>();
     186           0 :     throw py::type_error(oss.str());
     187           0 :   }
     188             : };
     189             : 
     190             : namespace {
     191        3084 : void expect_implicit_dimension(const Sizes &dims) {
     192             :   using std::to_string;
     193        3084 :   if (dims.size() == 0)
     194           0 :     throw except::DimensionError("Slicing a scalar object is not possible.");
     195        3084 :   if (dims.size() > 1) {
     196             :     std::string msg("Slicing with implicit dimension label is only possible "
     197           6 :                     "for 1-D objects. Got " +
     198          12 :                     to_string(dims) + " with ndim=" + to_string(dims.size()) +
     199           6 :                     ". Provide an explicit dimension label, e.g., var['" +
     200           6 :                     to_string(*dims.begin()) + "', 0] instead of var[0].");
     201           3 :     throw except::DimensionError(msg);
     202           3 :   }
     203        3081 : }
     204             : 
     205        2430 : void expect_positional_index(const py::slice &py_slice) {
     206        7288 :   for (const auto &key : {"start", "stop"}) {
     207        4859 :     if (const auto index = py::getattr(py_slice, key); !index.is_none()) {
     208             :       try {
     209        2330 :         static_cast<void>(index.cast<Variable>());
     210           1 :         throw except::DimensionError(
     211           2 :             "Dimension must be specified when indexing with a label.");
     212        2330 :       } catch (const py::cast_error &) {
     213        2329 :       }
     214        4859 :     }
     215             :   }
     216        2429 : }
     217             : 
     218             : template <class T>
     219          68 : T slice_by_list(const T &obj,
     220             :                 const std::tuple<Dim, std::vector<scipp::index>> &index) {
     221         142 :   const auto make_slice = [](scipp::index p, const scipp::index s) {
     222         142 :     const auto positive_p = p < 0 ? s + p : p;
     223         142 :     if (positive_p < 0 || positive_p >= s)
     224           0 :       throw std::out_of_range("The requested index " + std::to_string(p) +
     225             :                               " is out of range for dimension of length " +
     226             :                               std::to_string(s));
     227         142 :     return std::tuple{positive_p, positive_p + 1};
     228             :   };
     229          68 :   const auto &[dim, indices] = index;
     230          68 :   const auto size = obj.dims()[dim];
     231          68 :   if (!indices.empty()) {
     232          68 :     const auto [min, max] = std::minmax_element(indices.begin(), indices.end());
     233          68 :     if (*min < -size || *max >= size) {
     234           4 :       const auto bad = *min < -size ? *min : *max;
     235           4 :       throw_index_error(bad, size);
     236             :     }
     237             :   }
     238          64 :   std::vector<scipp::index_pair> ranges;
     239          64 :   ranges.reserve(indices.size());
     240         206 :   for (const auto &pos : indices) {
     241         142 :     const auto [start, stop] = make_slice(pos, size);
     242           0 :     ranges.emplace_back(static_cast<scipp::index>(start),
     243         142 :                         static_cast<scipp::index>(stop));
     244             :   }
     245          64 :   return extract_ranges(makeVariable<scipp::index_pair>(
     246         128 :                             Dims{dim}, Shape{ranges.size()}, Values(ranges)),
     247         128 :                         obj, dim);
     248          64 : }
     249             : } // namespace
     250             : 
     251             : template <class T, class... Ignored>
     252           9 : void bind_slice_methods(pybind11::class_<T, Ignored...> &c) {
     253             :   // Slice with implicit dim possible only if there is exactly one dimension. We
     254             :   // do *not* use the numpy/xarray mechanism which slices the outer dimension in
     255             :   // this case, since we consider it dangerous, leading to hard to find bugs.
     256          38 :   c.def("__getitem__", [](T &self, const scipp::index &index) {
     257         592 :     expect_implicit_dimension(self.dims());
     258         592 :     return getitem(self, {self.dim(), index});
     259             :   });
     260        2136 :   c.def("__getitem__", [](T &self, const py::slice &index) {
     261        2411 :     expect_implicit_dimension(self.dims());
     262        2411 :     expect_positional_index(index);
     263        2410 :     return getitem(self, {self.dim(), index});
     264             :   });
     265             :   // Note the order of overloads: For some reason pybind11(?) calls `len()` on
     266             :   // __getitem__ arguments when there is an overload accepting std::tuple. This
     267             :   // fails for scalar variables, so we place this before those overloads.
     268           9 :   c.def(
     269             :       "__getitem__",
     270         402 :       [](T &self, const Variable &condition) {
     271         402 :         return extract(self, condition);
     272             :       },
     273           0 :       py::call_guard<py::gil_scoped_release>());
     274             :   // These overloads must also be placed before the ones accepting std::tuple.
     275             :   // Otherwise, pybind11 would call `int()` on the 2nd tuple element
     276             :   // because of `Variable.__int__`.
     277             :   //
     278             :   // Note that label-based indexing with an implicit unique dim is not supported
     279             :   // for now. Labels can also be taken from any other coord, so if the coord
     280             :   // label is omitted we would have to "default" to using the dimension coord.
     281             :   // This may be too confusing, so we do not implement this for now. Note that
     282             :   // the objection to this is not absolute (unlike in the case of slicing outer
     283             :   // dimension above).
     284             :   if constexpr (std::is_same_v<T, DataArray>) {
     285           3 :     c.def("__getitem__", &slicer<T>::get_by_value);
     286           3 :     c.def("__setitem__", &slicer<T>::template set_by_value<Variable>);
     287           3 :     c.def("__setitem__", &slicer<T>::template set_by_value<DataArray>);
     288             :   }
     289             :   if constexpr (std::is_same_v<T, Dataset>) {
     290           3 :     c.def("__getitem__", &slicer<T>::get_by_value);
     291           3 :     c.def("__setitem__", &slicer<T>::template set_by_value<Dataset>);
     292             :   } else {
     293        1289 :     c.def("__len__", [](const T &self) {
     294        1350 :       if (self.dims().ndim() == 0)
     295           2 :         throw except::TypeError("len() of scalar object");
     296        1348 :       return self.dims().size(0);
     297             :     });
     298           7 :     c.def("_ipython_key_completions_", [](T &self) {
     299           2 :       py::typing::List<py::str> out;
     300           6 :       for (const auto &dim : self.dims()) {
     301           4 :         out.append(dim.name());
     302             :       }
     303           2 :       return out;
     304           0 :     });
     305             :   }
     306           9 :   c.def("__getitem__",
     307        1649 :         [](T &self, std::tuple<std::string, scipp::index> index) {
     308        1649 :           return getitem(self, to_dim_type(std::move(index)));
     309             :         });
     310         360 :   c.def("__getitem__", [](T &self, std::tuple<std::string, py::slice> index) {
     311         785 :     return getitem(self, to_dim_type(std::move(index)));
     312             :   });
     313           9 :   c.def("__getitem__", [](T &self, const py::ellipsis &index) {
     314           0 :     return getitem(self, index);
     315             :   });
     316           9 :   c.def("__setitem__",
     317         106 :         [](T &self, const scipp::index &index, const py::object &data) {
     318          53 :           expect_implicit_dimension(self.dims());
     319          53 :           slicer<T>::template set<std::tuple<Dim, scipp::index>>(
     320         106 :               self, {self.dim(), index}, data);
     321             :         });
     322           9 :   c.def("__setitem__",
     323          38 :         [](T &self, const py::slice &index, const py::object &data) {
     324          19 :           expect_implicit_dimension(self.dims());
     325          19 :           expect_positional_index(index);
     326          19 :           slicer<T>::template set<std::tuple<Dim, py::slice>>(
     327          38 :               self, {self.dim(), index}, data);
     328             :         });
     329          97 :   c.def("__setitem__", [](T &self, std::tuple<std::string, scipp::index> index,
     330             :                           const py::object &data) {
     331         397 :     slicer<T>::template set(self, to_dim_type(std::move(index)), data);
     332             :   });
     333          39 :   c.def("__setitem__", [](T &self, std::tuple<std::string, py::slice> index,
     334             :                           const py::object &data) {
     335          49 :     slicer<T>::template set(self, to_dim_type(std::move(index)), data);
     336             :   });
     337           9 :   c.def("__setitem__", &slicer<T>::template set<py::ellipsis>);
     338           9 :   c.def(
     339             :       "__getitem__",
     340           9 :       [](T &self, const std::vector<scipp::index> &indices) {
     341           9 :         expect_implicit_dimension(self.dims());
     342           6 :         return slice_by_list(self, {self.dim(), indices});
     343             :       },
     344           0 :       py::call_guard<py::gil_scoped_release>());
     345           9 :   c.def(
     346             :       "__getitem__",
     347          62 :       [](T &self, std::tuple<std::string, std::vector<scipp::index>> indices) {
     348          62 :         return slice_by_list<T>(self, to_dim_type(std::move(indices)));
     349             :       },
     350           9 :       py::call_guard<py::gil_scoped_release>());
     351           9 : }

Generated by: LCOV version 1.14