Repository: apytypes/apytypes (language: python)

Issue:
Support creating arrays from most array libraries
Closes #330

Seems like this only works for double/float64 arrays.


Discussion / hints:

Fix patch:
diff --git a/docs/api/apyfixedarray.rst b/docs/api/apyfixedarray.rst
index 60c78ae22..3e1e620ee 100644
--- a/docs/api/apyfixedarray.rst
+++ b/docs/api/apyfixedarray.rst
@@ -11,6 +11,8 @@
    Creation from other types
    -------------------------
 
+   .. automethod:: from_array
+
    .. automethod:: from_float
 
    Change word length
diff --git a/docs/api/apyfloatarray.rst b/docs/api/apyfloatarray.rst
index d4b419fc4..bfaf42ae6 100644
--- a/docs/api/apyfloatarray.rst
+++ b/docs/api/apyfloatarray.rst
@@ -11,6 +11,8 @@
    Creation from other types
    -------------------------
 
+   .. automethod:: from_array
+
    .. automethod:: from_float
 
    Change word length
diff --git a/src/apyfixedarray.cc b/src/apyfixedarray.cc
index 5fe8028b9..8d10d47b7 100644
--- a/src/apyfixedarray.cc
+++ b/src/apyfixedarray.cc
@@ -881,22 +881,9 @@ APyFixedArray APyFixedArray::from_double(
 )
 {
     if (nb::isinstance<nb::ndarray<nb::numpy>>(python_seq)) {
-        // Sequence is NumPy NDArray. Initialize using `_set_values_from_numpy_ndarray`
-        auto ndarray = nb::cast<nb::ndarray<nb::numpy>>(python_seq);
-        std::size_t ndim = ndarray.ndim();
-        if (ndim == 0) {
-            throw nb::type_error(
-                "APyFixedArray.from_float(): NDArray with ndim == 0 not supported"
-            );
-        }
-        std::vector<std::size_t> shape(ndim, 0);
-        for (std::size_t i = 0; i < ndim; i++) {
-            shape[i] = ndarray.shape(i);
-        }
-
-        APyFixedArray result(shape, int_bits, frac_bits, bits);
-        result._set_values_from_numpy_ndarray(ndarray);
-        return result;
+        // Sequence is NumPy NDArray. Initialize using `from_array`
+        auto ndarray = nb::cast<nb::ndarray<>>(python_seq);
+        return from_array(ndarray, int_bits, frac_bits, bits);
     }
 
     APyFixedArray result(
@@ -938,6 +925,29 @@ APyFixedArray APyFixedArray::from_double(
     return result;
 }
 
+APyFixedArray APyFixedArray::from_array(
+    const nb::ndarray<>& ndarray,
+    std::optional<int> int_bits,
+    std::optional<int> frac_bits,
+    std::optional<int> bits
+)
+{
+    std::size_t ndim = ndarray.ndim();
+    if (ndim == 0) {
+        throw nb::type_error(
+            "APyFixedArray.from_float(): NDArray with ndim == 0 not supported"
+        );
+    }
+    std::vector<std::size_t> shape(ndim, 0);
+    for (std::size_t i = 0; i < ndim; i++) {
+        shape[i] = ndarray.shape(i);
+    }
+
+    APyFixedArray result(shape, int_bits, frac_bits, bits);
+    result._set_values_from_ndarray(ndarray);
+    return result;
+}
+
 /* ********************************************************************************** *
  * *                            Private member functions                            * *
  * ********************************************************************************** */
@@ -1454,8 +1464,7 @@ void APyFixedArray::_set_bits_from_numpy_ndarray(const nb::ndarray<nb::numpy>& n
     );
 }
 
-void APyFixedArray::_set_values_from_numpy_ndarray(const nb::ndarray<nb::numpy>& ndarray
-)
+void APyFixedArray::_set_values_from_ndarray(const nb::ndarray<>& ndarray)
 {
 #define CHECK_AND_SET_VALUES_FROM_NPTYPE(__TYPE__)                                     \
     do {                                                                               \
@@ -1503,7 +1512,7 @@ void APyFixedArray::_set_values_from_numpy_ndarray(const nb::ndarray<nb::numpy>&
     // 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(
-        "APyFixedArray::_set_values_from_numpy_ndarray(): "
+        "APyFixedArray::_set_values_from_ndarray(): "
         "unsupported `dtype` expecting integer/float"
     );
 }
diff --git a/src/apyfixedarray.h b/src/apyfixedarray.h
index 3c4336599..4bcc983db 100644
--- a/src/apyfixedarray.h
+++ b/src/apyfixedarray.h
@@ -201,6 +201,14 @@ class APyFixedArray : public APyBuffer<mp_limb_t> {
         std::optional<int> bits = std::nullopt
     );
 
+    //! Create an `APyFixedArray` tensor object initialized with values from an ndarray
+    static APyFixedArray from_array(
+        const nanobind::ndarray<>& double_seq,
+        std::optional<int> int_bits = std::nullopt,
+        std::optional<int> frac_bits = std::nullopt,
+        std::optional<int> bits = std::nullopt
+    );
+
 private:
     /* ****************************************************************************** *
      * *                          Private member functions                          * *
@@ -320,8 +328,7 @@ class APyFixedArray : public APyBuffer<mp_limb_t> {
      * elements in `ndarray` are explicitly converted to `double` before being copied
      * into `*this`.
      */
-    void
-    _set_values_from_numpy_ndarray(const nanobind::ndarray<nanobind::numpy>& ndarray);
+    void _set_values_from_ndarray(const nanobind::ndarray<>& ndarray);
 
     //! Check if object are of the same type, i.e., same number of bits.
     APY_INLINE bool same_type_as(const APyFixedArray& other) const
diff --git a/src/apyfixedarray_wrapper.cc b/src/apyfixedarray_wrapper.cc
index cdfd09eba..1c2a2097f 100644
--- a/src/apyfixedarray_wrapper.cc
+++ b/src/apyfixedarray_wrapper.cc
@@ -394,6 +394,39 @@ void bind_fixed_array(nb::module_& m)
                 )
             )pbdoc"
         )
+        .def_static(
+            "from_array",
+            &APyFixedArray::from_array,
+            nb::arg("ndarray"),
+            nb::arg("int_bits") = nb::none(),
+            nb::arg("frac_bits") = nb::none(),
+            nb::arg("bits") = nb::none(),
+            R"pbdoc(
+            Create an :class:`APyFixedArray` object from an ndarray.
+
+            The initialized fixed-point values are the one closest to the
+            input floating-point value, rounded away from zero on ties. Exactly two of
+            the three bit-specifiers (`bits`, `int_bits`, `frac_bits`) has to be set.
+
+            Using NumPy arrays as input is in general faster than e.g. lists.
+
+            Parameters
+            ----------
+            ndarray : ndarray
+                Values to initialize from. The tensor shape will be taken from the ndarray shape.
+            int_bits : int, optional
+                Number of integer bits in the created fixed-point tensor
+            frac_bits : int, optional
+                Number of fractional bits in the created fixed-point tensor
+            bits : int, optional
+                Total number of bits in the created fixed-point tensor
+
+            Returns
+            -------
+            :class:`APyFixedArray`
+
+            )pbdoc"
+        )
 
         /*
          * Dunder methods
diff --git a/src/apyfloatarray.cc b/src/apyfloatarray.cc
index af46cfd71..84157c206 100644
--- a/src/apyfloatarray.cc
+++ b/src/apyfloatarray.cc
@@ -1050,28 +1050,18 @@ APyFloatArray APyFloatArray::from_double(
     std::optional<exp_t> bias
 )
 {
+    if (nb::isinstance<nb::ndarray<nb::numpy>>(double_seq)) {
+        // Sequence is NumPy NDArray. Initialize using `from_array`.
+        auto ndarray = nb::cast<nb::ndarray<>>(double_seq);
+        return from_array(ndarray, exp_bits, man_bits, bias);
+    }
+
     if (bias.has_value() && bias.value() != APyFloat::ieee_bias(exp_bits)) {
         throw NotImplementedException(
             "Not implemented: APyFloatArray with non IEEE-like bias"
         );
     }
 
-    if (nb::isinstance<nb::ndarray<nb::numpy>>(double_seq)) {
-        // Sequence is NumPy NDArray. Initialize using
-        // `_set_values_from_numpy_ndarray`
-        auto ndarray = nb::cast<nb::ndarray<nb::numpy>>(double_seq);
-        std::size_t ndim = ndarray.ndim();
-        assert(ndim > 0);
-        std::vector<std::size_t> shape(ndim, 0);
-        for (std::size_t i = 0; i < ndim; i++) {
-            shape[i] = ndarray.shape(i);
-        }
-
-        APyFloatArray result(shape, exp_bits, man_bits, bias);
-        result._set_values_from_numpy_ndarray(ndarray);
-        return result;
-    }
-
     APyFloatArray result(
         python_sequence_extract_shape(double_seq), exp_bits, man_bits, bias
     );
@@ -1098,8 +1088,32 @@ APyFloatArray APyFloatArray::from_double(
     return result;
 }
 
-void APyFloatArray::_set_values_from_numpy_ndarray(const nb::ndarray<nb::numpy>& ndarray
+APyFloatArray APyFloatArray::from_array(
+    const nanobind::ndarray<>& ndarray,
+    std::uint8_t exp_bits,
+    std::uint8_t man_bits,
+    std::optional<exp_t> bias
 )
+{
+    if (bias.has_value() && bias.value() != APyFloat::ieee_bias(exp_bits)) {
+        throw NotImplementedException(
+            "Not implemented: APyFloatArray with non IEEE-like bias"
+        );
+    }
+
+    std::size_t ndim = ndarray.ndim();
+    assert(ndim > 0);
+    std::vector<std::size_t> shape(ndim, 0);
+    for (std::size_t i = 0; i < ndim; i++) {
+        shape[i] = ndarray.shape(i);
+    }
+
+    APyFloatArray result(shape, exp_bits, man_bits, bias);
+    result._set_values_from_ndarray(ndarray);
+    return result;
+}
+
+void APyFloatArray::_set_values_from_ndarray(const nb::ndarray<>& ndarray)
 {
     // Double value used for converting.
     APyFloat double_caster(11, 52, 1023);
@@ -1141,7 +1155,7 @@ void APyFloatArray::_set_values_from_numpy_ndarray(const nb::ndarray<nb::numpy>&
     // 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_values_from_numpy_ndarray(): "
+        "APyFloatArray::_set_values_from_ndarray(): "
         "unsupported `dtype` expecting integer/float"
     );
 }
diff --git a/src/apyfloatarray.h b/src/apyfloatarray.h
index 94ddd47dc..e4f6c7b27 100644
--- a/src/apyfloatarray.h
+++ b/src/apyfloatarray.h
@@ -102,8 +102,15 @@ class APyFloatArray {
         std::optional<exp_t> bias = std::nullopt
     );
 
-    void
-    _set_values_from_numpy_ndarray(const nanobind::ndarray<nanobind::numpy>& ndarray);
+    //! Create an `APyFloatArray` tensor object initialized with values from an ndarray
+    static APyFloatArray from_array(
+        const nanobind::ndarray<>& ndarray,
+        std::uint8_t exp_bits,
+        std::uint8_t man_bits,
+        std::optional<exp_t> bias = std::nullopt
+    );
+
+    void _set_values_from_ndarray(const nanobind::ndarray<>& ndarray);
 
     //! Transposition function. For a 1-D array, returns an exact copy of `*this`. For
     //! a 2-D array, returns the matrix transposition of `*this`.
diff --git a/src/apyfloatarray_wrapper.cc b/src/apyfloatarray_wrapper.cc
index 8cb7cfe04..644cc141d 100644
--- a/src/apyfloatarray_wrapper.cc
+++ b/src/apyfloatarray_wrapper.cc
@@ -355,6 +355,33 @@ void bind_float_array(nb::module_& m)
                 )
             )pbdoc"
         )
+        .def_static(
+            "from_array",
+            &APyFloatArray::from_array,
+            nb::arg("ndarray"),
+            nb::arg("exp_bits") = nb::none(),
+            nb::arg("man_bits") = nb::none(),
+            nb::arg("bias") = nb::none(),
+            R"pbdoc(
+            Create an :class:`APyFloatArray` object from an ndarray.
+
+            Parameters
+            ----------
+            ndarray : ndarray
+                Values to initialize from. The tensor shape will be taken from the ndarray shape.
+            exp_bits : int
+                Number of exponent bits in the created fixed-point tensor
+            man_bits : int, optional
+                Number of mantissa bits in the created fixed-point tensor
+            bias : int, optional
+                Bias in the created fixed-point tensor
+
+            Returns
+            -------
+            :class:`APyFloatArray`
+
+            )pbdoc"
+        )
 
         /*
          * Dunder methods


Test patch:
diff --git a/lib/test/apyfixedarray/test_constructors.py b/lib/test/apyfixedarray/test_constructors.py
index 3051c6e9e..b9e289840 100644
--- a/lib/test/apyfixedarray/test_constructors.py
+++ b/lib/test/apyfixedarray/test_constructors.py
@@ -82,6 +82,30 @@ def test_numpy_creation(dt):
     assert np.all(a.to_numpy() == anp.astype(np.double))
 
 
+@pytest.mark.parametrize(
+    "dt",
+    [
+        "int64",
+        "int32",
+        "int16",
+        "int8",
+        "uint64",
+        "uint32",
+        "uint16",
+        "uint8",
+        "float64",
+        "float32",
+    ],
+)
+def test_numpy_creation_from_array(dt):
+    np = pytest.importorskip("numpy")
+    anp = np.array([[1, 2, 3, 4]], dtype=dt)
+
+    a = APyFixedArray.from_array(anp, 5, 4)
+
+    assert np.all(a.to_numpy() == anp.astype(np.double))
+
+
 def test_incorrect_double_construction():
     with pytest.raises(ValueError, match="Cannot convert nan to fixed-point"):
         APyFixedArray.from_float([float("NaN"), 0.3], 4, 4)
@@ -136,9 +160,7 @@ def test_base_constructor_ndarray(dt):
 def test_from_numpy_raises():
     np = pytest.importorskip("numpy")
     a = np.asarray([[0.0, 1.0]], dtype="half")
-    with pytest.raises(
-        TypeError, match="APyFixedArray::_set_values_from_numpy_ndarray"
-    ):
+    with pytest.raises(TypeError, match="APyFixedArray::_set_values_from_ndarray"):
         b = APyFixedArray.from_float(a, 14, 4)
     a = np.asarray([[0.0, 1.0]])
     with pytest.raises(TypeError, match="APyFixedArray::_set_bits_from_numpy_ndarray"):
diff --git a/lib/test/apyfloatarray/test_constructors.py b/lib/test/apyfloatarray/test_constructors.py
index 192e7788d..21bce436f 100644
--- a/lib/test/apyfloatarray/test_constructors.py
+++ b/lib/test/apyfloatarray/test_constructors.py
@@ -102,6 +102,30 @@ def test_numpy_creation(dt):
     assert np.all(a.to_numpy() == anp.astype(np.float64))
 
 
+@pytest.mark.parametrize(
+    "dt",
+    [
+        "int64",
+        "int32",
+        "int16",
+        "int8",
+        "uint64",
+        "uint32",
+        "uint16",
+        "uint8",
+        "float64",
+        "float32",
+    ],
+)
+def test_numpy_array_creation(dt):
+    np = pytest.importorskip("numpy")
+    anp = np.array([[1, 2, 3, 4]], dtype=dt)
+
+    a = APyFloatArray.from_array(anp, 4, 2)
+
+    assert np.all(a.to_numpy() == anp.astype(np.float64))
+
+
 def test_special_value_numpy_from_double():
     np = pytest.importorskip("numpy")
     a = np.asarray([[1e-323, float("inf")], [float("nan"), 0.0]])
@@ -120,7 +144,5 @@ def test_special_value_numpy_from_double():
 def test_from_numpy_raises():
     np = pytest.importorskip("numpy")
     a = np.asarray([[1e-323, float("inf")], [float("nan"), 0.0]], dtype="half")
-    with pytest.raises(
-        TypeError, match="APyFloatArray::_set_values_from_numpy_ndarray"
-    ):
+    with pytest.raises(TypeError, match="APyFloatArray::_set_values_from_ndarray"):
         b = APyFloatArray.from_float(a, 14, 60)