LCOV - code coverage report
Current view: top level - python - variable_init.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 145 159 91.2 %
Date: 2024-11-17 01:47:58 Functions: 56 56 100.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 Jan-Lukas Wynen
       5             : 
       6             : #include "pybind11.h"
       7             : 
       8             : #include "scipp/core/dtype.h"
       9             : #include "scipp/core/eigen.h"
      10             : #include "scipp/core/tag_util.h"
      11             : #include "scipp/dataset/dataset.h"
      12             : #include "scipp/units/string.h"
      13             : #include "scipp/variable/structures.h"
      14             : #include "scipp/variable/to_unit.h"
      15             : #include "scipp/variable/variable.h"
      16             : 
      17             : #include "dtype.h"
      18             : #include "format.h"
      19             : #include "numpy.h"
      20             : #include "py_object.h"
      21             : #include "unit.h"
      22             : 
      23             : using namespace scipp;
      24             : using namespace scipp::variable;
      25             : 
      26             : namespace py = pybind11;
      27             : 
      28             : namespace {
      29      119426 : bool is_empty(const py::object &sequence) {
      30      119426 :   if (py::isinstance<py::buffer>(sequence)) {
      31           0 :     return sequence.attr("ndim").cast<scipp::index>() == 0;
      32             :   }
      33      119426 :   return !py::bool_{sequence};
      34             : }
      35             : 
      36       22574 : auto shape_of(const py::object &array) { return py::iter(array.attr("shape")); }
      37             : 
      38           2 : scipp::index n_remaining(const py::iterator &it) {
      39           2 :   return std::distance(it, it.end());
      40             : }
      41             : 
      42           1 : [[noreturn]] void throw_ndim_mismatch_error(const scipp::index a_ndim,
      43             :                                             const std::string_view a_name,
      44             :                                             const scipp::index b_ndim,
      45             :                                             const std::string_view b_name) {
      46           1 :   throw std::invalid_argument(
      47           2 :       python::format("The number of dimensions in '", a_name, "' (", a_ndim,
      48             :                      ") does not match the number of dimensions in '", b_name,
      49           2 :                      "' (", b_ndim, ")."));
      50             : }
      51             : 
      52       21768 : void ensure_same_shape(const py::object &values, const py::object &variances) {
      53       21768 :   if (values.is_none() || variances.is_none()) {
      54       21371 :     return;
      55             :   }
      56             : 
      57         397 :   auto val_shape = shape_of(values);
      58         397 :   auto var_shape = shape_of(variances);
      59             : 
      60         397 :   scipp::index dim = 0;
      61         397 :   std::tuple<scipp::index, scipp::index, scipp::index> mismatch{-1, -1, -1};
      62        1085 :   for (; val_shape != val_shape.end() && var_shape != var_shape.end();
      63         688 :        ++val_shape, ++var_shape, ++dim) {
      64         688 :     if (val_shape->cast<scipp::index>() != var_shape->cast<scipp::index>()) {
      65           0 :       if (std::get<0>(mismatch) == -1) {
      66             :         // Defer throwing to let ndim error take precedence.
      67           0 :         mismatch = std::tuple{dim, val_shape->cast<scipp::index>(),
      68           0 :                               var_shape->cast<scipp::index>()};
      69             :       }
      70             :     }
      71             :   }
      72         397 :   if (val_shape != val_shape.end() || var_shape != var_shape.end()) {
      73           0 :     throw_ndim_mismatch_error(dim + n_remaining(val_shape), "values",
      74           0 :                               dim + n_remaining(var_shape), "variances");
      75             :   }
      76         397 :   if (std::get<0>(mismatch) != -1) {
      77           0 :     throw std::invalid_argument(python::format(
      78             :         "The shapes of 'values' and 'variances' differ in dimension ",
      79           0 :         std::get<0>(mismatch), ": ", std::get<1>(mismatch), " vs ",
      80           0 :         std::get<2>(mismatch), '.'));
      81             :   }
      82         397 : }
      83             : 
      84             : namespace detail {
      85       21780 : void consume_extra_dims(py::iterator &shape_it,
      86             :                         const scipp::index n_extra_dims) {
      87       21862 :   for (scipp::index i = 0; i < n_extra_dims; ++i) {
      88          82 :     if (shape_it == shape_it.end())
      89           0 :       throw std::invalid_argument(
      90           0 :           "Data has too few dimensions for given dimension labels.");
      91          82 :     ++shape_it;
      92             :   }
      93       21780 : }
      94             : 
      95       21780 : Dimensions build_dimensions(py::iterator &&label_it, py::iterator &&shape_it,
      96             :                             const scipp::index n_extra_dims,
      97             :                             const std::string_view shape_name) {
      98       21780 :   Dimensions dims;
      99       21780 :   scipp::index dim = 0;
     100       47410 :   for (; label_it != label_it.end() && shape_it != shape_it.end();
     101       25630 :        ++label_it, ++shape_it, ++dim) {
     102       25630 :     dims.addInner(Dim{label_it->cast<std::string>()},
     103             :                   shape_it->cast<scipp::index>());
     104             :   }
     105       21780 :   consume_extra_dims(shape_it, n_extra_dims);
     106       21780 :   if (label_it != label_it.end() || shape_it != shape_it.end()) {
     107           1 :     throw_ndim_mismatch_error(dim + n_remaining(label_it), "dims",
     108           1 :                               dim + n_remaining(shape_it), shape_name);
     109             :   }
     110       21779 :   return dims;
     111           1 : }
     112             : } // namespace detail
     113             : 
     114       39898 : Dimensions build_dimensions(const py::object &dim_labels,
     115             :                             const py::object &values,
     116             :                             const py::object &variances,
     117             :                             const scipp::index n_extra_dims = 0) {
     118       39898 :   if (is_empty(dim_labels)) {
     119       18118 :     return Dimensions{};
     120             :   } else {
     121       21780 :     if (!values.is_none()) {
     122       21768 :       ensure_same_shape(values, variances);
     123       43538 :       return detail::build_dimensions(py::iter(dim_labels), shape_of(values),
     124       65304 :                                       n_extra_dims, "values");
     125             :     } else {
     126          24 :       return detail::build_dimensions(py::iter(dim_labels), shape_of(variances),
     127          36 :                                       n_extra_dims, "variances");
     128             :     }
     129             :   }
     130             : }
     131             : 
     132       79528 : py::object parse_data_sequence(const py::object &dim_labels,
     133             :                                const py::object &data) {
     134             :   // Need to check for None because py::array does not preserve it.
     135       79528 :   if (is_empty(dim_labels) || data.is_none()) {
     136       57415 :     return data;
     137             :   } else {
     138       22113 :     return py::array(data);
     139             :   }
     140             : }
     141             : 
     142        2489 : void ensure_is_scalar(const py::buffer &array) {
     143        2489 :   if (const auto ndim = array.attr("ndim").cast<int64_t>(); ndim != 0) {
     144           1 :     throw except::DimensionError(python::format(
     145           2 :         "Cannot interpret ", ndim, "-dimensional array as a scalar."));
     146             :   }
     147        2488 : }
     148             : 
     149             : template <class T>
     150       18026 : T extract_scalar(const py::object &obj, const units::Unit unit) {
     151             :   using TM = ElementTypeMap<T>;
     152             :   using PyType = typename TM::PyType;
     153       18026 :   TM::check_assignable(obj, unit);
     154       18026 :   if (py::isinstance<py::buffer>(obj)) {
     155        2189 :     ensure_is_scalar(obj);
     156        2187 :     return converting_cast<PyType>::cast(obj.attr("item")());
     157             :   } else {
     158       15838 :     return converting_cast<PyType>::cast(obj);
     159             :   }
     160             : }
     161             : 
     162             : template <>
     163         331 : core::time_point extract_scalar<core::time_point>(const py::object &obj,
     164             :                                                   const units::Unit unit) {
     165             :   using TM = ElementTypeMap<core::time_point>;
     166             :   using PyType = typename TM::PyType;
     167         331 :   TM::check_assignable(obj, unit);
     168         331 :   if (py::isinstance<py::buffer>(obj)) {
     169         301 :     ensure_is_scalar(obj);
     170         903 :     return core::time_point{obj.attr("astype")(py::dtype::of<PyType>())
     171         602 :                                 .attr("item")()
     172         301 :                                 .template cast<PyType>()};
     173             :   } else {
     174          30 :     return core::time_point{obj.cast<PyType>()};
     175             :   }
     176             : }
     177             : 
     178             : template <>
     179          43 : python::PyObject extract_scalar<python::PyObject>(const py::object &obj,
     180             :                                                   const units::Unit unit) {
     181             :   using TM = ElementTypeMap<python::PyObject>;
     182          43 :   TM::check_assignable(obj, unit);
     183          43 :   return obj;
     184             : }
     185             : 
     186             : template <class T>
     187       40681 : auto make_element_array(const Dimensions &dims, const py::object &source,
     188             :                         const units::Unit unit) {
     189       40681 :   if (source.is_none()) {
     190          20 :     return element_array<T>();
     191       40661 :   } else if (dims.ndim() == 0) {
     192       36546 :     return element_array<T>(1, extract_scalar<T>(source, unit));
     193             :   } else {
     194       22261 :     element_array<T> array(dims.volume(), core::init_for_overwrite);
     195       22261 :     copy_array_into_view(cast_to_array_like<T>(source, unit), array, dims);
     196       22261 :     return array;
     197       22261 :   }
     198             : }
     199             : 
     200             : template <class T> struct MakeVariable {
     201       39748 :   static Variable apply(const Dimensions &dims, const py::object &values,
     202             :                         const py::object &variances, const units::Unit unit) {
     203       39748 :     const auto [values_unit, final_unit] = common_unit<T>(values, unit);
     204       39747 :     auto values_array =
     205             :         Values(make_element_array<T>(dims, values, values_unit));
     206       79492 :     auto variable = variances.is_none()
     207       38961 :                         ? makeVariable<T>(dims, std::move(values_array))
     208             :                         // cppcheck-suppress accessMoved  # False-positive.
     209         785 :                         : makeVariable<T>(dims, std::move(values_array),
     210       41320 :                                           Variances(make_element_array<T>(
     211             :                                               dims, variances, values_unit)));
     212       39742 :     variable.setUnit(values_unit);
     213       79484 :     return to_unit(variable, final_unit, CopyPolicy::TryAvoid);
     214       39746 :   }
     215             : };
     216             : 
     217       39764 : Variable make_variable(const py::object &dim_labels, const py::object &values,
     218             :                        const py::object &variances,
     219             :                        const std::optional<units::Unit> &unit_, DType dtype) {
     220       39764 :   const auto converted_values = parse_data_sequence(dim_labels, values);
     221       39764 :   const auto converted_variances = parse_data_sequence(dim_labels, variances);
     222       39764 :   dtype = common_dtype(converted_values, converted_variances, dtype);
     223             :   const auto dims =
     224       39749 :       build_dimensions(dim_labels, converted_values, converted_variances);
     225       39748 :   const auto unit = unit_.value_or(variable::default_unit_for(dtype));
     226             :   return core::CallDType<double, float, int64_t, int32_t, bool,
     227             :                          scipp::core::time_point, std::string, Variable,
     228             :                          DataArray, Dataset,
     229             :                          python::PyObject>::apply<MakeVariable>(dtype, dims,
     230             :                                                                 values,
     231             :                                                                 variances,
     232       79490 :                                                                 unit);
     233       39792 : }
     234             : 
     235         110 : template <int N> Dimensions pad_structure_dimensions(Dimensions dims) {
     236         110 :   dims.addInner(Dim::InternalStructureComponent, N);
     237         110 :   return dims;
     238             : }
     239             : 
     240          39 : template <int M, int N> Dimensions pad_structure_dimensions(Dimensions dims) {
     241          39 :   dims.addInner(Dim::InternalStructureRow, M);
     242          39 :   dims.addInner(Dim::InternalStructureColumn, N);
     243          39 :   return dims;
     244             : }
     245             : 
     246             : template <class T, class Elem, int... N>
     247         149 : Variable make_structured_variable(const py::object &dim_labels,
     248             :                                   const py::object &values_,
     249             :                                   const py::object &variances,
     250             :                                   const std::optional<units::Unit> &unit_) {
     251         149 :   if (!variances.is_none())
     252           0 :     throw except::VariancesError("Variances not supported for dtype " +
     253             :                                  to_string(dtype<Elem>));
     254             : 
     255         149 :   const auto values = py::array(values_);
     256         149 :   const auto unit = unit_.value_or(variable::default_unit_for(dtype<Elem>));
     257         149 :   const auto dims =
     258         149 :       build_dimensions(dim_labels, values, py::none(), sizeof...(N));
     259         149 :   const auto padded_dims = pad_structure_dimensions<N...>(dims);
     260             : 
     261         149 :   auto var = variable::make_structures<T, Elem>(
     262             :       dims, unit, make_element_array<Elem>(padded_dims, values, unit));
     263         298 :   return var;
     264         149 : }
     265             : } // namespace
     266             : 
     267             : /*
     268             :  * It is the init method's responsibility to check that the combination
     269             :  * of arguments is valid. Functions down the line do not check again.
     270             :  */
     271           3 : void bind_init(py::class_<Variable> &cls) {
     272           6 :   cls.def(
     273           3 :       py::init([](const py::object &dim_labels, const py::object &values,
     274             :                   const py::object &variances, const ProtoUnit unit,
     275             :                   const py::object &dtype, const bool aligned) {
     276       40005 :         if (values.is_none() && variances.is_none()) {
     277           0 :           throw std::invalid_argument(
     278           0 :               "At least one argument of 'values' and 'variances' is required.");
     279             :         }
     280       39913 :         const auto [scipp_dtype, actual_unit] =
     281       40005 :             cast_dtype_and_unit(dtype, unit);
     282             : 
     283       39913 :         auto var = [&, scipp_dtype = scipp_dtype, actual_unit = actual_unit]() {
     284       39913 :           if (scipp_dtype == ::dtype<Eigen::Vector3d>)
     285             :             return make_structured_variable<Eigen::Vector3d, double, 3>(
     286          86 :                 dim_labels, values, variances, actual_unit);
     287       39827 :           if (scipp_dtype == ::dtype<Eigen::Matrix3d>)
     288             :             return make_structured_variable<Eigen::Matrix3d, double, 3, 3>(
     289          29 :                 dim_labels, values, variances, actual_unit);
     290       39798 :           if (scipp_dtype == ::dtype<Eigen::Affine3d>)
     291             :             return make_structured_variable<Eigen::Affine3d, double, 4, 4>(
     292          10 :                 dim_labels, values, variances, actual_unit);
     293       39788 :           if (scipp_dtype == ::dtype<core::Quaternion>)
     294             :             return make_structured_variable<core::Quaternion, double, 4>(
     295          15 :                 dim_labels, values, variances, actual_unit);
     296       39773 :           if (scipp_dtype == ::dtype<core::Translation>)
     297             :             return make_structured_variable<core::Translation, double, 3>(
     298           9 :                 dim_labels, values, variances, actual_unit);
     299             : 
     300       39764 :           return make_variable(dim_labels, values, variances, actual_unit,
     301       39764 :                                scipp_dtype);
     302       39935 :         }();
     303             : 
     304       39891 :         var.set_aligned(aligned);
     305       79782 :         return var;
     306             :       }),
     307           6 :       py::kw_only(), py::arg("dims"), py::arg("values") = py::none(),
     308           6 :       py::arg("variances") = py::none(), py::arg("unit") = DefaultUnit{},
     309           6 :       py::arg("dtype") = py::none(), py::arg("aligned") = true,
     310             :       R"raw(
     311             : Initialize a variable with values and/or variances.
     312             : 
     313             : At least one argument of ``values`` and ``variances`` must be used.
     314             : if you want to preallocate memory to fill later, use :py:func:`scipp.empty`.
     315             : 
     316             : Attention
     317             : ---------
     318             : This constructor is meant primarily for internal use.
     319             : Use one of the Specialized
     320             : `creation functions <../../reference/creation-functions.rst>`_ instead.
     321             : See in particular :py:func:`scipp.array` and :py:func:`scipp.scalar`.
     322             : 
     323             : Parameters
     324             : ----------
     325             : dims:
     326             :    Dimension labels.
     327             : values:
     328             :    Sequence of values for constructing an array variable.
     329             : variances:
     330             :    Sequence of variances for constructing an array variable.
     331             : unit:
     332             :    Physical unit, defaults to ``scipp.units.dimensionless``.
     333             : dtype:
     334             :    Type of the variable's elements. Is deduced from other arguments
     335             :    in most cases. Defaults to ``sc.DType.float64`` if no deduction is
     336             :    possible.
     337             : aligned:
     338             :    Initial value for the alignment flag.
     339             : )raw");
     340           3 : }

Generated by: LCOV version 1.14