Repository: ivy-llc/ivy
Pull request: added rfftn function at backend
Close #18035 

Problem statement:
rfftn
 propose adding the RFFTN function to Ivy's Experimental API., I have been working on the RFFT2 function for the NumPy frontend for over a month now, and I've come across some complexities while iterating over multiple axes within the RFFT2 function.

Considering this, I would like to suggest exploring the implementation of the NumPy RFFT2 function in Ivy's backend using existing NumPy functions. By adding the RFFTN function to the backend, we can simplify the RFFT2 implementation, as RFFT2 can be considered a subset of RFFTN. We will need to handle this difference in the backend logic.

I understand that adding around 30 lines of logic to the frontend goes against Ivy's frontend design policy. However, by incorporating the RFFTN function in the backend, we can maintain consistency with other backends and simplify the frontend implementation of RFFT2.

Hints from issue discussion:

Commit message: 3 tests passing except tf

Patch to review:
diff --git a/ivy/data_classes/array/experimental/layers.py b/ivy/data_classes/array/experimental/layers.py
index f9092b71cd227..d595e245a45a8 100644
--- a/ivy/data_classes/array/experimental/layers.py
+++ b/ivy/data_classes/array/experimental/layers.py
@@ -1024,3 +1024,35 @@ def ifftn(
                 [-0.48472244+0.30233797j]])
         """
         return ivy.ifftn(self._data, s=s, axes=axes, norm=norm, out=out)
+
+    def rfftn(
+        self: ivy.Array,
+        s: Sequence[int] = None,
+        axes: Sequence[int] = None,
+        *,
+        norm: str = "backward",
+        out: Optional[ivy.Array] = None,
+    ) -> ivy.Array:
+        """
+        Compute the n-dimensional discrete Fourier Transform.
+
+        Parameters
+        ----------
+        self
+            Input array.
+        s
+            Shape (length of each transformed axis) of the output.
+        axes
+            Axes over which to compute the RFFT. If not given, the last len(s) axes are
+            used.
+        norm
+            Normalization mode: "backward", "ortho", or "forward".
+        out
+            Optional output array for writing the result.
+
+        Returns
+        -------
+        ret
+            The result of the RFFT operation.
+        """
+        return ivy.rfftn(self._data, s=s, axes=axes, norm=norm, out=out)
diff --git a/ivy/data_classes/container/experimental/layers.py b/ivy/data_classes/container/experimental/layers.py
index 3e675b89ea33d..394ca6fa63e7e 100644
--- a/ivy/data_classes/container/experimental/layers.py
+++ b/ivy/data_classes/container/experimental/layers.py
@@ -2019,3 +2019,112 @@ def ifftn(
             norm=norm,
             out=out,
         )
+
+    @staticmethod
+    def static_rfftn(
+        x: ivy.Container,
+        s: Optional[Union[int, Tuple[int, ...]]] = None,
+        axes: Optional[Union[int, Tuple[int, ...]]] = None,
+        *,
+        norm: str = "backward",
+        key_chains: Optional[Union[List[str], Dict[str, str]]] = None,
+        to_apply: bool = True,
+        prune_unapplied: bool = False,
+        map_sequences: bool = False,
+        out: Optional[ivy.Container] = None,
+    ) -> ivy.Container:
+        """
+        ivy.Container static method variant of ivy.rfftn.
+
+        This method simply wraps the function, and so the docstring for
+        ivy.rfftn also applies to this method with minimal changes.
+
+        Parameters
+        ----------
+        x
+            Input array of real numbers.
+
+        s
+            sequence of ints, optional
+            Shape (length of transformed axis) to use from the input (`s[0]` refers to
+            axis 0,`s[1]` to axis 1, etc.). The final element of `s` corresponds to `n`
+            for `rfft(x, n)`, while for the remaining axes, it corresponds to `n` for
+            `fft(x, n)`. Along any axis, if the given shape is smaller than that of the
+            input, the input is cropped. If it is larger,the input is padded with zeros.
+            If `s` is not given, the shape of the input along the axes specified by
+            `axes` is used.
+
+        axes
+            sequence of ints, optional
+            Axes over which to compute the FFT. If not given, the last `len(s)` axes
+            are used, or all axes if `s` is also not specified.
+
+        norm
+            Optional argument, "backward", "ortho" or "forward".
+            Defaults to be "backward".
+            "backward" indicates no normalization.
+            "ortho" indicates normalization by 1/sqrt(n).
+            "forward" indicates normalization by 1/n.
+
+        out
+            Optional output array, for writing the result to. It must have a shape that
+            the inputs broadcast to.
+
+        Returns
+        -------
+        ret
+            The truncated or zero-padded input, transformed along the axes indicated by
+            `axes` or by a combination of `s` or `x`, as explained in the parameters
+            section above.
+        """
+        return ContainerBase.cont_multi_map_in_function(
+            "rfftn",
+            x,
+            s=s,
+            axes=axes,
+            norm=norm,
+            key_chains=key_chains,
+            to_apply=to_apply,
+            prune_unapplied=prune_unapplied,
+            map_sequences=map_sequences,
+            out=out,
+        )
+
+    def rfftn(
+        self: Union[ivy.Array, ivy.NativeArray, ivy.Container],
+        s: Optional[Sequence[int]] = None,
+        axes: Optional[Union[int, Tuple[int]]] = None,
+        *,
+        norm: str = "backward",
+        out: Optional[ivy.Array] = None,
+    ) -> ivy.Container:
+        """
+        Compute the n-dimensional discrete Fourier Transform for real input.
+
+        Parameters
+        ----------
+        axes : int or tuple of ints, optional
+            Axes over which to compute the FFT. If not given, the last `n` axes are
+            used.
+        s : sequence of ints, optional
+            Shape (length of each transformed axis) of the output. Along each axis,
+            if the given shape is smaller than
+            that of the input, the input is cropped. If it is larger, the input
+            is padded with zeros.
+        norm : {'backward', 'ortho', 'forward'}, optional
+            Normalization mode. Default is 'backward'.
+        out : array-like, optional
+            Output array. Must have the same shape and type as the expected output.
+
+        Returns
+        -------
+        transformed : Container
+            The n-dimensional discrete Fourier Transform of the input.
+        """
+        return self.static_rfftn(
+            self,
+            s=s,
+            axes=axes,
+            norm=norm,
+            out=out,
+        )
diff --git a/ivy/functional/backends/jax/experimental/layers.py b/ivy/functional/backends/jax/experimental/layers.py
index 6ddbe8ea7ca8d..94b6e50f58a3d 100644
--- a/ivy/functional/backends/jax/experimental/layers.py
+++ b/ivy/functional/backends/jax/experimental/layers.py
@@ -799,3 +799,36 @@ def embedding(
             norms < -max_norm, embeddings * -max_norm / norms, embeddings
         )
     return embeddings
+
+
+@with_unsupported_dtypes({"0.4.13 and below": ("float16", "complex")}, backend_version)
+def rfftn(
+    x: JaxArray,
+    s: Sequence[int] = None,
+    axes: Sequence[int] = None,
+    *,
+    norm: str = "backward",
+    out: Optional[JaxArray] = None,
+) -> JaxArray:
+    if not all(isinstance(j, int) for j in axes):
+        raise ivy.utils.exceptions.IvyError(
+            f"Expecting {axes} to be a sequence of integers <class integer>"
+        )
+    if s is None:
+        s = (x.shape[axes[0]], x.shape[axes[1]])
+    if all(j < -len(x.shape) for j in s):
+        raise ivy.utils.exceptions.IvyError(
+            f"Invalid dim {axes}, expecting ranging"
+            f" from {-len(x.shape)} to {len(x.shape)-1}"
+        )
+    if not all(isinstance(j, int) for j in s):
+        raise ivy.utils.exceptions.IvyError(
+            f"Expecting {s} to be a sequence of integers <class integer>"
+        )
+    if all(j <= 1 for j in s):
+        raise ivy.utils.exceptions.IvyError(
+            f"Invalid data points {s}, expecting s points larger than 1"
+        )
+    if norm != "backward" and norm != "ortho" and norm != "forward":
+        raise ivy.utils.exceptions.IvyError(f"Unrecognized normalization mode {norm}")
+    return jnp.fft.rfftn(x, s, axes, norm).astype(jnp.complex128)
diff --git a/ivy/functional/backends/numpy/experimental/layers.py b/ivy/functional/backends/numpy/experimental/layers.py
index 24666fea02f7a..bbd5d596970b0 100644
--- a/ivy/functional/backends/numpy/experimental/layers.py
+++ b/ivy/functional/backends/numpy/experimental/layers.py
@@ -922,3 +922,35 @@ def embedding(
             norms < -max_norm, embeddings * -max_norm / norms, embeddings
         )
     return embeddings
+
+
+def rfftn(
+    x: np.ndarray,
+    s: Sequence[int] = None,
+    axes: Sequence[int] = None,
+    *,
+    norm: str = "backward",
+    out: Optional[np.ndarray] = None,
+) -> np.ndarray:
+    if not all(isinstance(j, int) for j in axes):
+        raise ivy.utils.exceptions.IvyError(
+            f"Expecting {axes} to be a sequence of integers <class integer>"
+        )
+    if s is None:
+        s = (x.shape[axes[0]], x.shape[axes[1]])
+    if all(j < -len(x.shape) for j in s):
+        raise ivy.utils.exceptions.IvyError(
+            f"Invalid dim {axes}, expecting ranging"
+            f" from {-len(x.shape)} to {len(x.shape)-1}  "
+        )
+    if not all(isinstance(j, int) for j in s):
+        raise ivy.utils.exceptions.IvyError(
+            f"Expecting {s} to be a sequence of integers <class integer>"
+        )
+    if all(j <= 1 for j in s):
+        raise ivy.utils.exceptions.IvyError(
+            f"Invalid data points {s}, expecting s points larger than 1"
+        )
+    if norm != "backward" and norm != "ortho" and norm != "forward":
+        raise ivy.utils.exceptions.IvyError(f"Unrecognized normalization mode {norm}")
+    return np.fft.rfftn(x, s, axes, norm).astype(np.complex128)
diff --git a/ivy/functional/backends/paddle/experimental/layers.py b/ivy/functional/backends/paddle/experimental/layers.py
index 3fda535f33616..45b2ffaa1da37 100644
--- a/ivy/functional/backends/paddle/experimental/layers.py
+++ b/ivy/functional/backends/paddle/experimental/layers.py
@@ -7,6 +7,7 @@
 from ivy.func_wrapper import (
     with_supported_device_and_dtypes,
 )
+from ivy.func_wrapper import with_unsupported_dtypes
 from .. import backend_version
 
 # local
@@ -365,3 +366,16 @@ def ifftn(
     out: Optional[paddle.Tensor] = None,
 ) -> paddle.Tensor:
     return paddle.fft.ifftn(x, s, axes, norm)
+
+
+@with_unsupported_dtypes({"0.4.13 and below": ("float32", "complex")}, backend_version)
+def rfftn(
+    x: paddle.Tensor,
+    s: Optional[Union[int, Tuple[int]]] = None,
+    axes: Optional[Union[int, Tuple[int]]] = None,
+    *,
+    norm: Optional[str] = "backward",
+    out: Optional[paddle.Tensor] = None,
+) -> paddle.Tensor:
+    result = paddle.fft.rfftn(x, s, axes, norm)
+    return result.astype("complex128")
diff --git a/ivy/functional/backends/tensorflow/experimental/layers.py b/ivy/functional/backends/tensorflow/experimental/layers.py
index 2bc60ea99c11b..bdf8d935ad3d4 100644
--- a/ivy/functional/backends/tensorflow/experimental/layers.py
+++ b/ivy/functional/backends/tensorflow/experimental/layers.py
@@ -1187,3 +1187,188 @@ def ifftn(
         return out
     else:
         return result
+
+
+"""
+RFFTN Function implementation
+"""
+
+
+# def _rfftn_norm(
+#     x: Union[tf.Tensor, tf.Variable],
+#     s: Sequence[int] = None,
+#     axes: Sequence[int] = None,
+#     norm: str = "backward",
+# ):
+#     n = tf.constant(s[-1] // 2 + 1, dtype=tf.complex128)
+#     if norm == "backward":
+#         return x
+#     elif norm == "ortho":
+#         return x / tf.sqrt(n)
+#     elif norm == "forward":
+#         return x / n
+#     else:
+#         raise ivy.utils.exceptions.IvyError(f"Unrecognized normalization mode {norm}")
+
+
+def rfft_input_validation(x):
+    if not x.dtype.is_floating:
+        raise TypeError(
+            "Invalid FFT input: `x` must be of a real dtype. Received: {}".format(
+                x.dtype
+            )
+        )
+    return x
+
+
+def rfft_operations(x, rank, norm_factor):
+    # if ivy.backend == 'tensorflow':
+    #     if x.dtype != tf.complex128:
+    #         x = tf.cast(x, tf.complex128)
+    if x.shape.rank == 1:
+        x = tf.signal.rfft(x)
+    elif x.shape.rank == 2:
+        x = tf.switch_case(
+            rank - 1, {0: lambda: tf.signal.rfft(x), 1: lambda: tf.signal.rfft2d(x)}
+        )
+    else:
+        x = tf.switch_case(
+            rank - 1,
+            {
+                0: lambda: tf.signal.rfft(x),
+                1: lambda: tf.signal.rfft2d(x),
+                2: lambda: tf.signal.rfft3d(x),
+            },
+        )
+    norm_factor = tf.cast(norm_factor, x.dtype)
+    x = x * norm_factor
+    return x
+
+
+def _rfftn_helper(x, shape, axes, norm):
+    # x = rfft_input_validation(tf.convert_to_tensor(x))
+    x = rfft_input_validation(x)
+    input_shape = x.shape
+    input_rank_tensor = tf.rank(x)
+
+    shape_, axes_ = shape_and_axes_validation(shape, axes, input_rank_tensor)
+
+    axes = axes_initialization(shape, axes, input_shape, input_rank_tensor)
+
+    perform_padding, perform_transpose = perform_actions_initialization(
+        shape, axes, input_shape, input_rank_tensor
+    )
+
+    shape = shape_initialization(shape, axes, x)
+
+    rank = rank_initialization(axes)
+
+    norm_factor = norm_initialization(norm, shape, x)
+
+    x = get_x_after_pad_or_crop(x, shape, axes, perform_padding, input_rank_tensor)
+
+    perm = get_perm(input_rank_tensor, axes)
+
+    x = transpose_x(x, perm, perform_transpose)
+
+    x = rfft_operations(x, rank, norm_factor)
+
+    x = transpose_x(x, tf.argsort(perm), perform_transpose)
+
+    x = tf.ensure_shape(x, static_output_shape(input_shape, shape_, axes_))
+
+    return x
+
+
+@with_unsupported_dtypes({"0.4.13 and below": ("float64", "complex")}, backend_version)
+def rfftn(
+    x: Union[tf.Tensor, tf.Variable],
+    s: Optional[Union[int, Tuple[int]]] = None,
+    axes: Optional[Union[int, Tuple[int]]] = None,
+    *,
+    norm: Optional[str] = "backward",
+    out: Optional[Union[tf.Tensor, tf.Variable]] = None,
+) -> Union[tf.Tensor, tf.Variable]:
+    result = _rfftn_helper(x, s, axes, norm)
+
+    if out is not None:
+        # out = tf.cast(result, tf.complex128)
+        out = result
+        return out
+    else:
+        return result
+        # return tf.cast(result, tf.complex128)
+
+
+# def _rfftn_norm(
+#     x: Union[tf.Tensor, tf.Variable],
+#     s: Sequence[int] = None,
+#     axes: Sequence[int] = None,
+#     norm: str = "backward",
+# ):
+#     n = tf.constant(s[-1] // 2 + 1, dtype=tf.complex128)
+#     if norm == "backward":
+#         return x
+#     elif norm == "ortho":
+#         return x / tf.sqrt(n)
+#     elif norm == "forward":
+#         return x / n
+#     else:
+#         raise ivy.utils.exceptions.IvyError(f"Unrecognized normalization mode {norm}")
+
+
+# @with_unsupported_dtypes(
+#     {"0.4.13 and below": ("float32", "complex")}, backend_version
+# )
+# def rfftn(
+#     x: Union[tf.Tensor, tf.Variable],
+#     s: Sequence[int] = None,
+#     axes: Sequence[int] = None,
+#     *,
+#     norm: str = "backward",
+#     out: Optional[Union[tf.Tensor, tf.Variable]] = None,
+# ) -> Union[tf.Tensor, tf.Variable]:
+#     if axes is None:
+#         axes = list(range(len(s)))
+#     elif s is None:
+#         s = [x.shape[axis] for axis in axes]
+#     elif len(s) != len(axes):
+#         raise ValueError("s and axes must have the same length.")
+
+#     if not all(isinstance(j, int) for j in s):
+#         raise ivy.utils.exceptions.IvyError(
+#             f"Expecting {s} to be a sequence of integers <class integer>"
+#         )
+#     if all(j <= 1 for j in s):
+#         raise ivy.utils.exceptions.IvyError(
+#             f"Invalid data points {s}, expecting s points larger than 1"
+#         )
+#     if norm != "backward" and norm != "ortho" and norm != "forward":
+#         raise ivy.utils.exceptions.IvyError(f"Unrecognized normalization mode {norm}")
+
+
+#     if len(s) != 2:
+#         raise ValueError("fft_length must have shape [2]")
+#     fft_length = tf.convert_to_tensor(s, dtype=tf.int32)
+
+#     # Convert s to a tensor with shape [2]
+#     # fft_length = tf.convert_to_tensor(s, dtype=tf.int32)
+#     # if tf.shape(fft_length).shape != [1] or tf.shape(fft_length).as_list()[0] != 2:
+#     #     raise ValueError("fft_length must have shape [2]")
+#     operation_name = f"RFFTn with s={s}, axes={axes}, norm={norm}"
+#     # tf_rfftn = tf.signal.rfft2d(x, fft_length=fft_length, name=operation_name)
+
+# # Reshape the input tensor to 2D
+#     x_2d = tf.reshape(x, [-1, s[0]])
+
+#     # Perform rfft2d operation on the reshaped tensor
+#     tf_rfftn_2d = tf.signal.rfft2d(x_2d, fft_length=s[1], name=operation_name)
+
+#     # Reshape the result back to the original shape
+#     output_shape = tf.concat([tf.shape(x)[:-1], [s[-1] // 2 + 1]], axis=0)
+#     tf_rfftn = tf.reshape(tf_rfftn_2d, output_shape)
+
+
+#     # Apply the same normalization as 'backward' in NumPy
+#     tf_rfftn = _rfftn_norm(tf_rfftn, s, axes, norm, out).astype("complex128")
+#     return tf_rfftn
diff --git a/ivy/functional/backends/torch/experimental/layers.py b/ivy/functional/backends/torch/experimental/layers.py
index 3403e0f0f52a6..872c597d456c6 100644
--- a/ivy/functional/backends/torch/experimental/layers.py
+++ b/ivy/functional/backends/torch/experimental/layers.py
@@ -907,3 +907,38 @@ def ifftn(
     out: Optional[torch.Tensor] = None,
 ) -> torch.Tensor:
     return torch.fft.ifftn(x, s=s, dim=axes, norm=norm, out=out)
+
+
+@with_unsupported_dtypes({"2.0.1 and below": ("bfloat16", "float16")}, backend_version)
+def rfftn(
+    x: torch.Tensor,
+    s: Sequence[int] = None,
+    axes: Sequence[int] = None,
+    *,
+    norm: str = "backward",
+    out: Optional[torch.Tensor] = None,
+) -> torch.Tensor:
+    if not all(isinstance(j, int) for j in axes):
+        raise ivy.utils.exceptions.IvyError(
+            f"Expecting {axes} to be a sequence of integers <class integer>"
+        )
+    if s is None:
+        s = (x.shape[axes[0]], x.shape[axes[1]])
+    if all(j < -len(x.shape) for j in s):
+        raise ivy.utils.exceptions.IvyError(
+            f"Invalid axes {axes}, expecting ranging"
+            f" from {-len(x.shape)} to {len(x.shape)-1}"
+        )
+    if not all(isinstance(j, int) for j in s):
+        raise ivy.utils.exceptions.IvyError(
+            f"Expecting {s} to be a sequence of integers <class integer>"
+        )
+    if all(j <= 1 for j in s):
+        raise ivy.utils.exceptions.IvyError(
+            f"Invalid data points {s}, expecting s points larger than 1"
+        )
+    if norm != "backward" and norm != "ortho" and norm != "forward":
+        raise ivy.utils.exceptions.IvyError(f"Unrecognized normalization mode {norm}")
+    return torch.tensor(
+        torch.fft.rfftn(x, s, axes, norm=norm, out=out), dtype=torch.complex128
+    )
diff --git a/ivy/functional/ivy/experimental/layers.py b/ivy/functional/ivy/experimental/layers.py
index b9c12b7a5a397..104580cc94742 100644
--- a/ivy/functional/ivy/experimental/layers.py
+++ b/ivy/functional/ivy/experimental/layers.py
@@ -2511,3 +2511,84 @@ def ifftn(
             [-0.48472244+0.30233797j]])
     """
     return ivy.current_backend(x).ifftn(x, s=s, axes=axes, norm=norm, out=out)
+
+
+@handle_exceptions
+@handle_nestable
+@handle_out_argument
+# @inputs_to_ivy_arrays
+@to_native_arrays_and_back
+def rfftn(
+    x: Union[ivy.Array, ivy.NativeArray],
+    s: Optional[Sequence[int]] = None,
+    axes: Optional[Sequence[int]] = None,
+    *,
+    norm: Optional[str] = None,
+    out: Optional[ivy.Array] = None,
+) -> ivy.Array:
+    """
+    Compute the N-dimensional discrete Fourier Transform for real input.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array, taken to be real.
+    s : sequence of ints, optional
+        Shape (length along each transformed axis) to use from the input.
+        (s[0] refers to axis 0, s[1] to axis 1, etc.). The final element of s
+        corresponds to n for rfft(x, n), while for the remaining axes, it
+        corresponds to n for fft(x, n). Along any axis, if the given shape is
+        smaller than that of the input, the input is cropped. If it is larger,
+        the input is padded with zeros. If s is not given, the shape of the
+        input along the axes specified by axes is used.
+    axes : sequence of ints, optional
+        Axes over which to compute the FFT. If not given, the last len(s) axes
+        are used, or all axes if s is also not specified.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode. Default is "backward". Indicates which direction of
+        the forward/backward pair of transforms is scaled and with what
+        normalization factor.
+    out : array_like, optional
+        Optional output array to store the result of the computation. The shape
+        and dtype of this array must match the expected output.
+
+    Returns
+    -------
+    out : complex ndarray
+        The truncated or zero-padded input, transformed along the axes indicated
+        by axes or by a combination of s and a, as explained in the parameters
+        section above. The length of the last axis transformed will be
+        s[-1] // 2 + 1, while the remaining transformed axes will have lengths
+        according to s, or unchanged from the input.
+
+    Raises
+    ------
+    ValueError
+        If s and axes have different lengths.
+    IndexError
+        If an element of axes is larger than the number of axes of a.
+
+    Examples
+    --------
+    >>> x = ivy.array([1, 2, 3, 4], dtype=ivy.float32)
+    >>> result = ivy.rfftn(x)
+    >>> print(result)
+    [10.+0.j  -2.+2.j   0.+0.j  -2.-2.j]
+
+    >>> x = ivy.array([[1, 2, 3], [4, 5, 6]], dtype=ivy.float32)
+    >>> result = ivy.rfftn(x, s=(3, 4), axes=(0, 1))
+    >>> print(result)
+    [[21. +0.j    0. +0.j    0. +0.j    0. +0.j   ]
+     [-1.5+1.299j -1.5+0.433j -1.5-0.433j -1.5-1.299j]]
+    """
+    if norm is None:
+        norm = "backward"
+
+    if axes is None:
+        axes = list(range(x.ndim - len(s), x.ndim))
+    elif s is None:
+        s = [x.shape[axis] for axis in axes]
+    elif len(s) != len(axes):
+        raise ValueError("s and axes must have the same length.")
+
+    return ivy.current_backend(x).rfftn(x, s=s, axes=axes, norm=norm, out=out)
diff --git a/ivy_tests/test_ivy/test_functional/test_experimental/test_nn/test_layers.py b/ivy_tests/test_ivy/test_functional/test_experimental/test_nn/test_layers.py
index 854fc8e6ba8eb..74310d8138070 100644
--- a/ivy_tests/test_ivy/test_functional/test_experimental/test_nn/test_layers.py
+++ b/ivy_tests/test_ivy/test_functional/test_experimental/test_nn/test_layers.py
@@ -1113,3 +1113,82 @@ def test_ifftn(
         axes=axes,
         norm=norm,
     )
+
+
+@st.composite
+def x_and_rfftn(draw):
+    min_rfftn_points = 2
+    dtype = draw(helpers.get_dtypes("float"))
+    x_dim = draw(
+        helpers.get_shape(
+            min_dim_size=2, max_dim_size=100, min_num_dims=2, max_num_dims=4
+        )
+    )
+    x = draw(
+        helpers.array_values(
+            dtype=dtype[0],
+            shape=tuple(x_dim),
+            min_value=-1e10,
+            max_value=1e10,
+            # # allow_inf=False,
+            large_abs_safety_factor=2.5,
+            small_abs_safety_factor=2.5,
+            safety_factor_scale="log",
+        )
+    )
+    # max_rfftn_points = min(256, max(x_dim) // 2 + 1)
+    # s = tuple(
+    #     draw(st.integers(min_rfftn_points, max_rfftn_points))
+    # for _ in range(len(x_dim))
+    # )
+    # axes = draw(
+    #     st.lists(
+    #         st.integers(-1, len(x_dim)), min_size=1, max_size=len(x_dim), unique=True
+    #     )
+    # )
+    axes = draw(
+        st.lists(
+            st.integers(0, len(x_dim) - 1), min_size=1, max_size=len(x_dim), unique=True
+        )
+    )
+    s = draw(
+        st.lists(
+            st.integers(min_rfftn_points, 256), min_size=len(axes), max_size=len(axes)
+        )
+    )
+    norm = draw(st.sampled_from(["backward", "forward", "ortho"]))
+    return dtype, x, s, axes, norm
+
+
+@handle_test(
+    fn_tree="functional.ivy.experimental.rfftn",
+    d_x_d_s_n=x_and_rfftn(),
+    ground_truth_backend="numpy",
+    # container_flags=st.just([False]),
+    test_gradients=st.just(False),
+)
+def test_rfftn(
+    *,
+    d_x_d_s_n,
+    test_flags,
+    backend_fw,
+    fn_name,
+    on_device,
+    ground_truth_backend,
+):
+    dtype, x, s, axes, norm = d_x_d_s_n
+    helpers.test_function(
+        ground_truth_backend=ground_truth_backend,
+        input_dtypes=dtype,
+        test_flags=test_flags,
+        fw=backend_fw,
+        on_device=on_device,
+        fn_name=fn_name,
+        rtol_=1e-3,
+        atol_=1e-3,
+        # atol_=1e-2,
+        x=x,
+        s=s,
+        axes=axes,
+        norm=norm,
+    )


Code review comments:
diff_hunk: @@ -365,3 +366,16 @@ def ifftn(
     out: Optional[paddle.Tensor] = None,
 ) -> paddle.Tensor:
     return paddle.fft.ifftn(x, s, axes, norm)
+
+
+@with_unsupported_dtypes({"0.4.13 and below": ("float32", "complex")}, backend_version)
+def rfftn(
+    x: paddle.Tensor,
+    s: Optional[Union[int, Tuple[int]]] = None,
+    axes: Optional[Union[int, Tuple[int]]] = None,
path: ivy/functional/backends/paddle/experimental/layers.py
text: @user1:
I believe the unsupported dtypes are float16 and complex

@author:
I have changed it to the abovee format as youcan see