Repository: apytypes/apytypes (language: python)

Issue:
`APyFloatArray.from_bits`
Just like `APyFloat.from_bits`, there should exist a corresponding function for `APyFloatArray`.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ce3432673..d92fc8ab0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   specifiers are given.
 - Added optional parameter for `bias` in array creation functions that were
   missing it.
+- Added `APyFloatArray.from_bits`.
 
 ### Fixed
 
diff --git a/lib/apytypes/_apytypes.pyi b/lib/apytypes/_apytypes.pyi
index 4ae825a73..64d877dcf 100644
--- a/lib/apytypes/_apytypes.pyi
+++ b/lib/apytypes/_apytypes.pyi
@@ -2560,6 +2560,36 @@ class APyFloatArray:
         :class:`APyFloatArray`
         """
 
+    @staticmethod
+    def from_bits(
+        bits: Sequence, exp_bits: int, man_bits: int, bias: int | None = None
+    ) -> APyFloatArray:
+        """
+        Create an :class:`APyFloatArray` object from bit-representations.
+
+        Parameters
+        ----------
+        bits : int
+            The bit-representations.
+        exp_bits : int
+            Number of exponent bits in the created floating-point tensor
+        man_bits : int
+            Number of mantissa bits in the created floating-point tensor
+        bias : int, optional
+            Bias. If not provided, *bias* is ``2**exp_bits - 1``.
+
+        Examples
+        --------
+
+        >>> from apytypes import APyFloatArray
+
+        >>> a = APyFloatArray.from_bits([[60, 61], [80, 82]], exp_bits=5, man_bits=2)
+
+        Returns
+        -------
+        :class:`APyFloatArray`
+        """
+
     @staticmethod
     def zeros(
         shape: tuple, exp_bits: int, man_bits: int, bias: int | None = None
diff --git a/src/apyfloat.cc b/src/apyfloat.cc
index 0b0d0e71b..d22a0013d 100644
--- a/src/apyfloat.cc
+++ b/src/apyfloat.cc
@@ -710,6 +710,21 @@ APyFloat& APyFloat::update_from_bits(nb::int_ python_long_int_bit_pattern)
     return *this;
 }
 
+APyFloat& APyFloat::update_from_bits(std::uint64_t bits)
+{
+    assert((1 + exp_bits + man_bits) <= 64);
+
+    man = bits & man_mask();
+    bits >>= man_bits;
+
+    exp = bits & exp_mask();
+    bits >>= exp_bits;
+
+    sign = bits & 1;
+
+    return *this;
+}
+
 nb::int_ APyFloat::to_bits() const
 {
     return apyfloat_to_bits({ sign, exp, man }, exp_bits, man_bits);
diff --git a/src/apyfloat.h b/src/apyfloat.h
index 22f5a1227..c7b6ec4af 100644
--- a/src/apyfloat.h
+++ b/src/apyfloat.h
@@ -367,6 +367,12 @@ class APyFloat {
     APyFloat
     construct_nan(std::optional<bool> new_sign = std::nullopt, man_t payload = 1) const;
 
+    //! Update data fields based on a bit pattern
+    APyFloat& update_from_bits(nb::int_ python_long_int_bit_pattern);
+
+    //! Update data fields based on a bit pattern
+    APyFloat& update_from_bits(std::uint64_t bits);
+
     //! Test if two floating-point numbers are identical, i.e., has the same value, and
     //! the same format
     bool is_identical(const APyFloat& other) const;
@@ -445,9 +451,6 @@ class APyFloat {
      * ******************************************************************************
      */
 
-    //! Update data fields based on a bit pattern
-    APyFloat& update_from_bits(nanobind::int_ python_long_int_bit_pattern);
-
     //! Create a bit mask for the exponent field
     APY_INLINE exp_t exp_mask() const { return ((1ULL << exp_bits) - 1); }
     //! Calculate the maximum value for the exponent field
diff --git a/src/apyfloatarray.cc b/src/apyfloatarray.cc
index 6c546314c..0a64cbfd7 100644
--- a/src/apyfloatarray.cc
+++ b/src/apyfloatarray.cc
@@ -1934,7 +1934,6 @@ APyFloatArray APyFloatArray::from_double(
     );
 
     const auto py_obj = python_sequence_walk<nb::float_, nb::int_>(number_seq);
-    const auto actual_bias = bias.value_or(APyFloat::ieee_bias(exp_bits));
 
     for (std::size_t i = 0; i < result._data.size(); i++) {
         if (nb::isinstance<nb::float_>(py_obj[i])) {
@@ -1942,13 +1941,13 @@ APyFloatArray APyFloatArray::from_double(
                                   (double)nb::cast<nb::float_>(py_obj[i]),
                                   exp_bits,
                                   man_bits,
-                                  actual_bias
+                                  result.bias
             )
                                   .get_data();
         } else if (nb::isinstance<nb::int_>(py_obj[i])) {
             result._data[i]
                 = APyFloat::from_integer(
-                      nb::cast<nb::int_>(py_obj[i]), exp_bits, man_bits, actual_bias
+                      nb::cast<nb::int_>(py_obj[i]), exp_bits, man_bits, result.bias
                 )
                       .get_data();
         } else {
@@ -1995,7 +1994,7 @@ void APyFloatArray::_set_values_from_ndarray(const nb::ndarray<nb::c_contig>& nd
                                          man_of_double(value) });                      \
                 APyFloat fp                                                            \
                     = double_caster.cast_from_double(exp_bits, man_bits, bias);        \
-                _data[i] = { fp.get_sign(), fp.get_exp(), fp.get_man() };              \
+                _data[i] = fp.get_data();                                              \
             }                                                                          \
             return; /* Conversion completed, exit function */                          \
         }                                                                              \
@@ -2026,6 +2025,90 @@ void APyFloatArray::_set_values_from_ndarray(const nb::ndarray<nb::c_contig>& nd
     );
 }
 
+APyFloatArray APyFloatArray::from_bits(
+    const nb::sequence& python_bit_patterns,
+    int exp_bits,
+    int man_bits,
+    std::optional<exp_t> bias
+)
+{
+    check_exponent_format(exp_bits);
+    check_mantissa_format(man_bits);
+
+    if (nb::isinstance<nb::ndarray<>>(python_bit_patterns)) { // ndarray
+        const auto ndarray = nb::cast<nb::ndarray<nb::c_contig>>(python_bit_patterns);
+
+        assert(ndarray.ndim() > 0);
+        std::vector<std::size_t> shape(ndarray.ndim(), 0);
+        for (std::size_t i = 0; i < ndarray.ndim(); i++) {
+            shape[i] = ndarray.shape(i);
+        }
+
+        APyFloatArray result(shape, exp_bits, man_bits, bias);
+        result._set_bits_from_ndarray(ndarray);
+        return result;
+    }
+
+    APyFloatArray result(
+        python_sequence_extract_shape(python_bit_patterns), exp_bits, man_bits, bias
+    );
+
+    const auto py_obj = python_sequence_walk<nb::float_, nb::int_>(python_bit_patterns);
+
+    APyFloat f(exp_bits, man_bits, result.bias);
+    for (std::size_t i = 0; i < result._data.size(); i++) {
+        if (nb::isinstance<nb::int_>(py_obj[i])) {
+            result._data[i]
+                = f.update_from_bits(nb::cast<nb::int_>(py_obj[i])).get_data();
+        } else {
+            throw std::domain_error("Invalid Python objects in sequence");
+        }
+    }
+
+    return result;
+}
+
+void APyFloatArray::_set_bits_from_ndarray(const nb::ndarray<nb::c_contig>& ndarray)
+{
+    // Double value used for converting.
+    APyFloat f(exp_bits, man_bits, bias);
+
+#define CHECK_AND_SET_VALUES_FROM_NPTYPE(__TYPE__)                                     \
+    do {                                                                               \
+        if (ndarray.dtype() == nb::dtype<__TYPE__>()) {                                \
+            const auto ndarray_view = ndarray.view<__TYPE__, nb::ndim<1>>();           \
+            for (std::size_t i = 0; i < ndarray.size(); i++) {                         \
+                const auto bits = static_cast<std::uint64_t>(ndarray_view.data()[i]);  \
+                f.update_from_bits(bits);                                              \
+                _data[i] = f.get_data();                                               \
+            }                                                                          \
+            return; /* Conversion completed, exit function */                          \
+        }                                                                              \
+    } while (0)
+
+    // Each `CHECK_AND_SET_VALUES_FROM_NPTYPE` checks the dtype of `ndarray` and
+    // converts all the data if it matches. If successful,
+    // `CHECK_AND_SET_VALUES_FROM_NPTYPES` returns. Otherwise, the next attempted
+    // conversion will take place
+    CHECK_AND_SET_VALUES_FROM_NPTYPE(std::int64_t);
+    CHECK_AND_SET_VALUES_FROM_NPTYPE(std::int32_t);
+    CHECK_AND_SET_VALUES_FROM_NPTYPE(std::int16_t);
+    CHECK_AND_SET_VALUES_FROM_NPTYPE(std::int8_t);
+    CHECK_AND_SET_VALUES_FROM_NPTYPE(std::uint64_t);
+    CHECK_AND_SET_VALUES_FROM_NPTYPE(std::uint32_t);
+    CHECK_AND_SET_VALUES_FROM_NPTYPE(std::uint16_t);
+    CHECK_AND_SET_VALUES_FROM_NPTYPE(std::uint8_t);
+
+    // None of the `CHECK_AND_VALUES_FROM_NPTYPE` succeeded. Unsupported type, throw
+    // an error. If possible, it would be nice to show a string representation of
+    // the `dtype`. Seems hard to achieve with nanobind, but please fix this if you
+    // find out how this can be achieved.
+    throw nb::type_error(
+        "APyFloatArray::_set_bits_from_ndarray(): "
+        "unsupported `dtype` expecting integer"
+    );
+}
+
 APyFloatArray APyFloatArray::broadcast_to(const std::vector<std::size_t> shape) const
 {
     if (!is_broadcastable(this->_shape, shape)) {
diff --git a/src/apyfloatarray.h b/src/apyfloatarray.h
index 47872fb5c..45db07a72 100644
--- a/src/apyfloatarray.h
+++ b/src/apyfloatarray.h
@@ -114,6 +114,16 @@ class APyFloatArray : public APyArray<APyFloatData, APyFloatArray> {
         std::optional<exp_t> bias = std::nullopt
     );
 
+    //! Create an `APyFloatArray` tensor object initialized from bit-representation
+    static APyFloatArray from_bits(
+        const nb::sequence& python_bit_patterns,
+        int exp_bits,
+        int man_bits,
+        std::optional<exp_t> bias = std::nullopt
+    );
+
+    void _set_bits_from_ndarray(const nanobind::ndarray<nanobind::c_contig>& ndarray);
+
     //! Set data fields based on an and-array of doubles
     void _set_values_from_ndarray(const nanobind::ndarray<nanobind::c_contig>& ndarray);
 
@@ -368,6 +378,14 @@ class APyFloatArray : public APyArray<APyFloatData, APyFloatArray> {
         std::optional<exp_t> bias = std::nullopt
     );
 
+    //! Create an `APyFloatArray` tensor object initialized from bit-representation
+    static APyFloatArray _from_bits_ndarray(
+        const nb::ndarray<nb::c_contig>& ndarray,
+        int exp_bits,
+        int man_bits,
+        std::optional<exp_t> bias = std::nullopt
+    );
+
     /*!
      * Evaluate the inner between two vectors. This method assumes that the the shape
      * of both `*this` and `rhs` are equally long. Anything else is undefined
diff --git a/src/apyfloatarray_wrapper.cc b/src/apyfloatarray_wrapper.cc
index 9e5977a30..d80506dbf 100644
--- a/src/apyfloatarray_wrapper.cc
+++ b/src/apyfloatarray_wrapper.cc
@@ -520,6 +520,43 @@ void bind_float_array(nb::module_& m)
             :class:`APyFloatArray`
             )pbdoc"
         )
+        .def_static(
+            "from_bits",
+            &APyFloatArray::from_bits,
+            nb::arg("bits"),
+            nb::arg("exp_bits"),
+            nb::arg("man_bits"),
+            nb::arg("bias") = nb::none(),
+            R"pbdoc(
+            Create an :class:`APyFloatArray` object from bit-representations.
+
+            Parameters
+            ----------
+            bits : int
+                The bit-representations.
+            exp_bits : int
+                Number of exponent bits in the created floating-point tensor
+            man_bits : int
+                Number of mantissa bits in the created floating-point tensor
+            bias : int, optional
+                Bias. If not provided, *bias* is ``2**exp_bits - 1``.
+
+            Examples
+            --------
+
+            >>> from apytypes import APyFloatArray
+
+            >>> a = APyFloatArray.from_bits(
+            ...     [[60, 61], [80, 82]],
+            ...     exp_bits=5,
+            ...     man_bits=2
+            ... )
+
+            Returns
+            -------
+            :class:`APyFloatArray`
+            )pbdoc"
+        )
         .def_static(
             "zeros",
             &APyFloatArray::zeros,


Test patch:
diff --git a/lib/test/apyfloatarray/test_methods.py b/lib/test/apyfloatarray/test_methods.py
index eeb182499..a2aeaa8ed 100644
--- a/lib/test/apyfloatarray/test_methods.py
+++ b/lib/test/apyfloatarray/test_methods.py
@@ -145,6 +145,68 @@ def test_array_from_array_raises():
         APyFloatArray.from_array(np.array([[1, 2, 3, 4]]), 5, 5).cast(5, -300)
 
 
+@pytest.mark.float_array
+def test_from_bits():
+    # Test raises
+    with pytest.raises(
+        ValueError,
+        match="Exponent bits must be a non-negative integer less or equal to .. but 300 was given",
+    ):
+        APyFloatArray.from_bits([0], 300, 5)
+
+    with pytest.raises(
+        ValueError,
+        match="python_sequence_extract_shape",
+    ):
+        APyFloatArray.from_bits(["0"], 5, 10)
+
+    with pytest.raises(
+        ValueError,
+        match="Invalid",
+    ):
+        APyFloatArray.from_bits([1, 10, 1.0, 5], 5, 10)
+
+    # 1-D
+    assert APyFloatArray.from_bits(range(2**8), 5, 2).to_bits() == list(range(2**8))
+
+    # 2-D
+    assert APyFloatArray.from_bits([[65, 66], [191, 190]], 5, 2, 9).is_identical(
+        APyFloatArray([[0, 0], [1, 1]], [[16, 16], [15, 15]], [[1, 2], [3, 2]], 5, 2, 9)
+    )
+
+    # Test long Python integer
+    val = 0xF000000FFFFFFFFFF
+    APyFloatArray.from_bits([val], 27, 40).is_identical(
+        APyFloatArray([1], [117440512], [1099511627775], 27, 40)
+    )
+
+
+@pytest.mark.float_array
+def test_from_bits_numpy():
+    # Skip this test if `NumPy` is not present on the machine
+    np = pytest.importorskip("numpy")
+
+    # Test raise
+    with pytest.raises(
+        TypeError,
+        match="APyFloatArray::_set_bits_from_ndarray()",
+    ):
+        APyFloatArray.from_bits(np.asarray([1.0]), 8, 15)
+
+    # 16-bit, (2, 2)
+    vals = np.asarray([[65, 66], [191, 190]], dtype=np.int16)
+    assert APyFloatArray.from_bits(vals, 5, 2, 9).is_identical(
+        APyFloatArray([[0, 0], [1, 1]], [[16, 16], [15, 15]], [[1, 2], [3, 2]], 5, 2, 9)
+    )
+
+    # 64-bit, (4, 4, 4)
+    a = APyFloatArray.from_float(
+        [1.0, -8564651213, 2**-1070, float("inf")] * 16, 11, 52
+    ).reshape((4, 4, 4))
+    nparr = a.to_bits(numpy=True)
+    assert APyFloatArray.from_bits(nparr, 11, 52).is_identical(a)
+
+
 @pytest.mark.float_array
 def test_to_numpy():
     # Skip this test if `NumPy` is not present on the machine