Repository: computationalgeography/lue (language: python)

Issue:
Add Python wrapper for `reclassify`
This is needed for an implementation of `lue.pcraster.timeinputscalar`.


Discussion / hints:

Fix patch:
diff --git a/source/framework/algorithm/include/lue/framework/algorithm/default_policies/reclassify.hpp b/source/framework/algorithm/include/lue/framework/algorithm/default_policies/reclassify.hpp
index c609abe87..38efb6dd4 100644
--- a/source/framework/algorithm/include/lue/framework/algorithm/default_policies/reclassify.hpp
+++ b/source/framework/algorithm/include/lue/framework/algorithm/default_policies/reclassify.hpp
@@ -26,5 +26,16 @@ namespace lue {
             return reclassify(Policies{}, array, lookup_table);
         }
 
+
+        template<typename FromElement, typename ToElement, Rank rank>
+        PartitionedArray<ToElement, rank> reclassify(
+            PartitionedArray<FromElement, rank> const& array,
+            LookupTable<FromElement, ToElement> const& lookup_table)
+        {
+            using Policies = policy::reclassify::DefaultPolicies<FromElement, ToElement>;
+
+            return reclassify(Policies{}, array, lookup_table);
+        }
+
     }  // namespace default_policies
 }  // namespace lue
diff --git a/source/framework/algorithm/include/lue/framework/algorithm/definition/reclassify.hpp b/source/framework/algorithm/include/lue/framework/algorithm/definition/reclassify.hpp
index 1f522198e..0f68165da 100644
--- a/source/framework/algorithm/include/lue/framework/algorithm/definition/reclassify.hpp
+++ b/source/framework/algorithm/include/lue/framework/algorithm/definition/reclassify.hpp
@@ -146,4 +146,4 @@ namespace lue {
     reclassify<ArgumentType<void(Policies)>, FromElement, ToElement, rank>(                                  \
         ArgumentType<void(Policies)> const&,                                                                 \
         PartitionedArray<FromElement, rank> const&,                                                          \
-        LookupTable<FromElement, ToElement> const&);
+        hpx::shared_future<LookupTable<FromElement, ToElement>> const&);
diff --git a/source/framework/algorithm/include/lue/framework/algorithm/reclassify.hpp b/source/framework/algorithm/include/lue/framework/algorithm/reclassify.hpp
index baebaa550..d6c1edd6a 100644
--- a/source/framework/algorithm/include/lue/framework/algorithm/reclassify.hpp
+++ b/source/framework/algorithm/include/lue/framework/algorithm/reclassify.hpp
@@ -14,7 +14,7 @@ namespace lue {
     PartitionedArray<ToElement, rank> reclassify(
         Policies const& policies,
         PartitionedArray<FromElement, rank> const& array,
-        hpx::future<LookupTable<FromElement, ToElement>> const& lookup_table);
+        hpx::shared_future<LookupTable<FromElement, ToElement>> const& lookup_table);
 
 
     template<typename Policies, typename FromElement, typename ToElement, Rank rank>
diff --git a/source/framework/algorithm/include/lue/framework/algorithm/value_policies/reclassify.hpp b/source/framework/algorithm/include/lue/framework/algorithm/value_policies/reclassify.hpp
index 90b73f488..3ce385137 100644
--- a/source/framework/algorithm/include/lue/framework/algorithm/value_policies/reclassify.hpp
+++ b/source/framework/algorithm/include/lue/framework/algorithm/value_policies/reclassify.hpp
@@ -26,5 +26,16 @@ namespace lue {
             return reclassify(Policies{}, array, lookup_table);
         }
 
+
+        template<typename FromElement, typename ToElement, Rank rank>
+        PartitionedArray<ToElement, rank> reclassify(
+            PartitionedArray<FromElement, rank> const& array,
+            LookupTable<FromElement, ToElement> const& lookup_table)
+        {
+            using Policies = policy::reclassify::DefaultValuePolicies<FromElement, ToElement>;
+
+            return reclassify(Policies{}, array, lookup_table);
+        }
+
     }  // namespace value_policies
 }  // namespace lue
diff --git a/source/framework/python/CMakeLists.txt b/source/framework/python/CMakeLists.txt
index d6f59fba4..fd0ceb9b3 100644
--- a/source/framework/python/CMakeLists.txt
+++ b/source/framework/python/CMakeLists.txt
@@ -85,6 +85,9 @@ add_library(lue_py_framework SHARED
     src/algorithm/focal_operation/focal_minimum.cpp
     src/algorithm/focal_operation/focal_sum.cpp
 
+    src/algorithm/global_operation.cpp
+    src/algorithm/global_operation/reclassify.cpp
+
     src/algorithm/local_operation.cpp
     src/algorithm/local_operation/logical_and.cpp
     src/algorithm/local_operation/logical_exclusive_or.cpp
@@ -164,6 +167,7 @@ target_include_directories(lue_py_framework
 target_link_libraries(lue_py_framework
     PRIVATE
         lue::framework_focal_operation
+        lue::framework_global_operation
         lue::framework_local_operation
         lue::framework_routing_operation
         lue::framework_zonal_operation
diff --git a/source/framework/python/src/algorithm/global_operation.cpp b/source/framework/python/src/algorithm/global_operation.cpp
new file mode 100644
index 000000000..bb2a5aebf
--- /dev/null
+++ b/source/framework/python/src/algorithm/global_operation.cpp
@@ -0,0 +1,14 @@
+#include <pybind11/pybind11.h>
+
+
+namespace lue::framework {
+
+    void bind_reclassify(pybind11::module& module);
+
+
+    void bind_global_operations(pybind11::module& module)
+    {
+        bind_reclassify(module);
+    }
+
+}  // namespace lue::framework
diff --git a/source/framework/python/src/algorithm/global_operation/reclassify.cpp b/source/framework/python/src/algorithm/global_operation/reclassify.cpp
new file mode 100644
index 000000000..da3861453
--- /dev/null
+++ b/source/framework/python/src/algorithm/global_operation/reclassify.cpp
@@ -0,0 +1,154 @@
+#include "lue/framework/algorithm/value_policies/reclassify.hpp"
+#include <pybind11/numpy.h>
+#include <pybind11/stl.h>
+
+
+using namespace pybind11::literals;
+
+
+namespace lue::framework {
+    namespace {
+
+        template<typename FromElement, typename ToElement, Rank rank>
+        PartitionedArray<ToElement, rank> reclassify2(
+            PartitionedArray<FromElement, rank> const& array,
+            LookupTable<FromElement, ToElement> const& lookup_table)
+        {
+            return value_policies::reclassify(array, lookup_table);
+        }
+
+
+        template<typename ToElement2, typename FromElement, typename ToElement1>
+        LookupTable<FromElement, ToElement2> cast_lut(
+            LookupTable<FromElement, ToElement1> const& lookup_table)
+        {
+            static_assert(std::is_integral_v<FromElement>);
+            static_assert(std::is_floating_point_v<ToElement1>);
+            static_assert(std::is_floating_point_v<ToElement2>);
+
+            if constexpr (std::is_same_v<ToElement1, ToElement2>)
+            {
+                return lookup_table;
+            }
+            else
+            {
+                LookupTable<FromElement, ToElement2> result;
+
+                for (auto const& [key, value] : lookup_table)
+                {
+                    result[key] = static_cast<ToElement2>(value);
+                }
+
+                return result;
+            }
+        }
+
+
+        template<typename FromElement, typename ToElement, Rank rank>
+        pybind11::object reclassify1(
+            PartitionedArray<FromElement, rank> const& array,
+            LookupTable<FromElement, ToElement> const& lookup_table,
+            pybind11::dtype const& dtype)
+        {
+            // Switch on dtype and call a function that returns an array of the
+            // right value type
+            auto const kind = dtype.kind();
+            auto const size = dtype.itemsize();  // bytes
+            pybind11::object result;
+
+            switch (kind)
+            {
+                case 'i':
+                {
+                    // Signed integer
+                    switch (size)
+                    {
+                        case 4:
+                        {
+                            break;
+                        }
+                        case 8:
+                        {
+                            break;
+                        }
+                    }
+
+                    break;
+                }
+                case 'u':
+                {
+                    // Unsigned integer
+                    switch (size)
+                    {
+                        case 1:
+                        {
+                            break;
+                        }
+                        case 4:
+                        {
+                            break;
+                        }
+                        case 8:
+                        {
+                            break;
+                        }
+                    }
+
+                    break;
+                }
+                case 'f':
+                {
+                    // Floating-point
+                    switch (size)
+                    {
+                        case 4:
+                        {
+                            result = pybind11::cast(reclassify2(array, cast_lut<float>(lookup_table)));
+                            break;
+                        }
+                        case 8:
+                        {
+                            result = pybind11::cast(reclassify2(array, cast_lut<double>(lookup_table)));
+                            break;
+                        }
+                    }
+
+                    break;
+                }
+            }
+
+            if (!result)
+            {
+                throw std::runtime_error(fmt::format("Unsupported dtype (kind={}, itemsize={})", kind, size));
+            }
+
+            return result;
+        }
+
+
+        template<typename FromElement, typename ToElement, Rank rank>
+        pybind11::object reclassify(
+            PartitionedArray<FromElement, rank> const& array,
+            LookupTable<FromElement, ToElement> const& lookup_table,
+            pybind11::object const& dtype_args)
+        {
+            return reclassify1(array, lookup_table, pybind11::dtype::from_args(dtype_args));
+        }
+
+    }  // Anonymous namespace
+
+
+    void bind_reclassify(pybind11::module& module)
+    {
+        // The lookup table is a Python dictionary. The dtype argument is to allow the caller
+        // to specify exactly what type to use for the resulting array.
+        module.def("reclassify", reclassify<std::uint8_t, double, 2>, "array"_a, "lookup_table"_a, "dtype"_a);
+        module.def(
+            "reclassify", reclassify<std::uint32_t, double, 2>, "array"_a, "lookup_table"_a, "dtype"_a);
+        module.def("reclassify", reclassify<std::int32_t, double, 2>, "array"_a, "lookup_table"_a, "dtype"_a);
+        module.def(
+            "reclassify", reclassify<std::uint64_t, double, 2>, "array"_a, "lookup_table"_a, "dtype"_a);
+        module.def("reclassify", reclassify<std::int64_t, double, 2>, "array"_a, "lookup_table"_a, "dtype"_a);
+    }
+
+}  // namespace lue::framework
diff --git a/source/framework/python/src/submodule.cpp b/source/framework/python/src/submodule.cpp
index 6541d083c..b4b285eea 100644
--- a/source/framework/python/src/submodule.cpp
+++ b/source/framework/python/src/submodule.cpp
@@ -47,6 +47,7 @@ namespace lue::framework {
     void bind_create_array(pybind11::module& module);
     void bind_wait(pybind11::module& module);
     void bind_focal_operations(pybind11::module& module);
+    void bind_global_operations(pybind11::module& module);
     void bind_local_operations(pybind11::module& module);
     void bind_routing_operations(pybind11::module& module);
     void bind_zonal_operations(pybind11::module& module);
@@ -87,6 +88,7 @@ namespace lue::framework {
         bind_create_array(submodule);
         bind_wait(submodule);
         bind_focal_operations(submodule);
+        bind_global_operations(submodule);
         bind_local_operations(submodule);
         bind_routing_operations(submodule);
         bind_zonal_operations(submodule);


Test patch:
diff --git a/source/framework/python/test/algorithm/global_operation/__init__.py b/source/framework/python/test/algorithm/global_operation/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/source/framework/python/test/algorithm/global_operation/reclassify_test.py b/source/framework/python/test/algorithm/global_operation/reclassify_test.py
new file mode 100644
index 000000000..eba00dc9d
--- /dev/null
+++ b/source/framework/python/test/algorithm/global_operation/reclassify_test.py
@@ -0,0 +1,35 @@
+import numpy as np
+
+import lue.framework as lfr
+import lue_test
+
+
+def setUpModule():
+    lue_test.start_hpx_runtime()
+
+
+def tearDownModule():
+    lue_test.stop_hpx_runtime()
+
+
+class ReclassifyTest(lue_test.TestCase):
+    @lue_test.framework_test_case
+    def test_overloads(self):
+
+        array_shape = (60, 40)
+        partition_shape = (10, 10)
+        id_ = 3
+
+        for from_type in [np.uint8, np.uint32, np.uint64, np.int32, np.int64]:
+            ids = lfr.create_array(array_shape, partition_shape, from_type, id_)
+
+            for to_type in [np.float32, np.float64]:
+                lookup_table = {
+                    1: 1.1,
+                    2: 2.2,
+                    3: 3.3,
+                    4: 4.4,
+                }
+                array = lfr.reclassify(ids, lookup_table, dtype=to_type)
+
+                self.assertEqual(array.dtype, np.dtype(to_type))