Repository: scipy/scipy

Issue:
idct normalization unexpected?
Is this intentional?

```
>>> from scipy.fftpack import dct, idct
>>> import numpy as np
>>> x = np.array([1., 2, 3])
>>> idct(dct(x))
array([  6.,  12.,  18.])
>>> idct(dct(x, norm='ortho'), norm='ortho')
array([ 1.,  2.,  3.])
```

Is it really a convention that `idct` does not have the correct scale factor so that `idct o dct` is an identity operation?



Discussion:
@cournape: this came up on stackoverflow --- do you remember if idct o dct is supposed to be normalized?

IIRC, I just copied the conventions used by FFTW: http://www.fftw.org/doc/1d-Real_002deven-DFTs-_0028DCTs_0029.html

Just noticed this issue: Is there interest in changing this behavior for `scipy.fft`?  I think that it's an obvious inconsistency with the other FFT functions. It's also worth noting that FFTW doesn't do _any_ normalisation for any of their FFT types.
> Is there interest in changing this behavior for scipy.fft? I think that it's an obvious inconsistency with the other FFT functions. It's also worth noting that FFTW doesn't do any normalisation for any of their FFT types.

Yes it would be nice to have consistency within the module as much as possible. We are creating a new module from scratch so this is our opportunity to fix past mistakes without dealing with deprecation problems.
@larsoner 

Isn't it already consistent?

```
>>> from scipy.fftpack import ifft, idct
>>> ifft(np.array([ 30.,  -8.,   6.,  -2.,   6.,  -8.])).real
array([  4.,   3.,   5.,  10.,   5.,   3.])
>>> idct(np.array([ 30.,  -8.,   6.,  -2.]), 1) / 6
array([  4.,   3.,   5.,  10.])
```

The documentation says that `norm='ortho'` is only for compatibility with Matlab?
```python
>>> idct(np.array([ 30.,  -8.,   6.,  -2.]), 1) / 6
array([  4.,   3.,   5.,  10.])
```

That extra `/ 6` at the end is exactly the inconsistency
Ohh, but `norm='ortho'` doesn't do the same thing as the `/6`, so it would need a different default normalization and still support `'ortho'`?

Resolving patch:
diff --git a/scipy/fft/_backend.py b/scipy/fft/_backend.py
--- a/scipy/fft/_backend.py
+++ b/scipy/fft/_backend.py
@@ -1,6 +1,7 @@
 import scipy._lib.uarray as ua
 import scipy.fftpack as _fftpack
 from . import _pocketfft
+from . import _realtransforms
 
 
 class _ScipyBackend:
@@ -15,10 +16,21 @@ class _ScipyBackend:
     """
     __ua_domain__ = "numpy.scipy.fft"
 
+    realtransforms = {
+        'dct': _fftpack.dct,
+        'dst': _fftpack.dst,
+        'dctn': _fftpack.dctn,
+        'dstn': _fftpack.dstn,
+        'idct': _realtransforms._idct,
+        'idst': _realtransforms._idst,
+        'idctn': _realtransforms._idctn,
+        'idstn': _realtransforms._idstn,
+    }
+
     @staticmethod
     def __ua_function__(method, args, kwargs):
         fn = (getattr(_pocketfft, method.__name__, None)
-              or getattr(_fftpack, method.__name__, None))
+              or _ScipyBackend.realtransforms.get(method.__name__, None))
 
         if fn is None:
             return NotImplemented
diff --git a/scipy/fft/_realtransforms.py b/scipy/fft/_realtransforms.py
--- a/scipy/fft/_realtransforms.py
+++ b/scipy/fft/_realtransforms.py
@@ -1,56 +1,638 @@
 import scipy.fftpack as _fftpack
 from ._basic import _dispatch
 from scipy._lib.uarray import Dispatchable
-from scipy._lib.doccer import doc_replace
 import numpy as np
-import functools
 
 __all__ = ['dct', 'idct', 'dst', 'idst', 'dctn', 'idctn', 'dstn', 'idstn']
 
 
+def _normalize_inverse(x, kind, type, axes):
+    """Normalizes the inverse transform inplace
+
+    Parameters
+    ----------
+    x : array_like
+        The output array of the unnormalized inverse transform, modified inplace
+    kind : {'s', 'c'}
+        Whether the trasform is discrete sine or cosine
+    type : {1, 2, 3, 4}
+        Type of DCT/DST
+    axes : int or array_like of ints or None
+        Axes along which the DCT/DST has been computed
+    """
+    if axes is None:
+        shape = np.array(x.shape)
+    else:
+        shape = np.take(x.shape, axes)
+
+    if type == 1:
+        if kind == 'c':
+            shape -= 1
+        else:
+            shape += 1
+
+    N = np.prod(2 * shape)
+    x /= N
+
+
 @_dispatch
-@doc_replace(_fftpack.dctn, 'fftpack', 'fft')
 def dctn(x, type=2, shape=None, axes=None, norm=None, overwrite_x=False):
+    """
+    Return multidimensional Discrete Cosine Transform along the specified axes.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DCT (see Notes). Default type is 2.
+    shape : int or array_like of ints or None, optional
+        The shape of the result.  If both `shape` and `axes` (see below) are
+        None, `shape` is ``x.shape``; if `shape` is None but `axes` is
+        not None, then `shape` is ``scipy.take(x.shape, axes, axis=0)``.
+        If ``shape[i] > x.shape[i]``, the i-th dimension is padded with zeros.
+        If ``shape[i] < x.shape[i]``, the i-th dimension is truncated to
+        length ``shape[i]``.
+        If any element of `shape` is -1, the size of the corresponding
+        dimension of `x` is used.
+    axes : int or array_like of ints or None, optional
+        Axes along which the DCT is computed.
+        The default is over all axes.
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    idctn : Inverse multidimensional DCT
+
+    Notes
+    -----
+    For full details of the DCT types and normalization modes, as well as
+    references, see `dct`.
+
+    Examples
+    --------
+    >>> from scipy.fft import dctn, idctn
+    >>> y = np.random.randn(16, 16)
+    >>> np.allclose(y, idctn(dctn(y)))
+    True
+
+    """
     return (Dispatchable(x, np.ndarray),)
 
 
 @_dispatch
-@doc_replace(_fftpack.idctn, 'fftpack', 'fft')
 def idctn(x, type=2, shape=None, axes=None, norm=None, overwrite_x=False):
+    """
+    Return multidimensional Discrete Cosine Transform along the specified axes.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DCT (see Notes). Default type is 2.
+    shape : int or array_like of ints or None, optional
+        The shape of the result.  If both `shape` and `axes` (see below) are
+        None, `shape` is ``x.shape``; if `shape` is None but `axes` is
+        not None, then `shape` is ``scipy.take(x.shape, axes, axis=0)``.
+        If ``shape[i] > x.shape[i]``, the i-th dimension is padded with zeros.
+        If ``shape[i] < x.shape[i]``, the i-th dimension is truncated to
+        length ``shape[i]``.
+        If any element of `shape` is -1, the size of the corresponding
+        dimension of `x` is used.
+    axes : int or array_like of ints or None, optional
+        Axes along which the IDCT is computed.
+        The default is over all axes.
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    dctn : multidimensional DCT
+
+    Notes
+    -----
+    For full details of the IDCT types and normalization modes, as well as
+    references, see `idct`.
+
+    Examples
+    --------
+    >>> from scipy.fft import dctn, idctn
+    >>> y = np.random.randn(16, 16)
+    >>> np.allclose(y, idctn(dctn(y)))
+    True
+
+    """
     return (Dispatchable(x, np.ndarray),)
 
 
+def _idctn(x, type=2, shape=None, axes=None, norm=None, overwrite_x=False):
+    y = _fftpack.idctn(x, type, shape, axes, norm, overwrite_x)
+    if norm is None:
+        _normalize_inverse(y, 'c', type, axes)
+    return y
+
+
 @_dispatch
-@doc_replace(_fftpack.dstn, 'fftpack', 'fft')
 def dstn(x, type=2, shape=None, axes=None, norm=None, overwrite_x=False):
+    """
+    Return multidimensional Discrete Sine Transform along the specified axes.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DST (see Notes). Default type is 2.
+    shape : int or array_like of ints or None, optional
+        The shape of the result.  If both `shape` and `axes` (see below) are
+        None, `shape` is ``x.shape``; if `shape` is None but `axes` is
+        not None, then `shape` is ``scipy.take(x.shape, axes, axis=0)``.
+        If ``shape[i] > x.shape[i]``, the i-th dimension is padded with zeros.
+        If ``shape[i] < x.shape[i]``, the i-th dimension is truncated to
+        length ``shape[i]``.
+        If any element of `shape` is -1, the size of the corresponding
+        dimension of `x` is used.
+    axes : int or array_like of ints or None, optional
+        Axes along which the DCT is computed.
+        The default is over all axes.
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    idstn : Inverse multidimensional DST
+
+    Notes
+    -----
+    For full details of the DST types and normalization modes, as well as
+    references, see `dst`.
+
+    Examples
+    --------
+    >>> from scipy.fft import dstn, idstn
+    >>> y = np.random.randn(16, 16)
+    >>> np.allclose(y, idstn(dstn(y)))
+    True
+
+    """
     return (Dispatchable(x, np.ndarray),)
 
 
 @_dispatch
-@doc_replace(_fftpack.idstn, 'fftpack', 'fft')
 def idstn(x, type=2, shape=None, axes=None, norm=None, overwrite_x=False):
+    """
+    Return multidimensional Discrete Sine Transform along the specified axes.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DST (see Notes). Default type is 2.
+    shape : int or array_like of ints or None, optional
+        The shape of the result.  If both `shape` and `axes` (see below) are
+        None, `shape` is ``x.shape``; if `shape` is None but `axes` is
+        not None, then `shape` is ``scipy.take(x.shape, axes, axis=0)``.
+        If ``shape[i] > x.shape[i]``, the i-th dimension is padded with zeros.
+        If ``shape[i] < x.shape[i]``, the i-th dimension is truncated to
+        length ``shape[i]``.
+        If any element of `shape` is -1, the size of the corresponding
+        dimension of `x` is used.
+    axes : int or array_like of ints or None, optional
+        Axes along which the IDST is computed.
+        The default is over all axes.
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    dstn : multidimensional DST
+
+    Notes
+    -----
+    For full details of the IDST types and normalization modes, as well as
+    references, see `idst`.
+
+    Examples
+    --------
+    >>> from scipy.fft import dstn, idstn
+    >>> y = np.random.randn(16, 16)
+    >>> np.allclose(y, idstn(dstn(y)))
+    True
+
+    """
     return (Dispatchable(x, np.ndarray),)
 
 
+def _idstn(x, type=2, shape=None, axes=None, norm=None, overwrite_x=False):
+    y = _fftpack.idstn(x, type, shape, axes, norm, overwrite_x)
+    if norm is None:
+        _normalize_inverse(y, 's', type, axes)
+    return y
+
+
 @_dispatch
-@doc_replace(_fftpack.dct, 'fftpack', 'fft')
 def dct(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False):
+    r"""
+    Return the Discrete Cosine Transform of arbitrary type sequence x.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DCT (see Notes). Default type is 2.
+    n : int, optional
+        Length of the transform.  If ``n < x.shape[axis]``, `x` is
+        truncated.  If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the dct is computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    idct : Inverse DCT
+
+    Notes
+    -----
+    For a single dimension array ``x``, ``dct(x, norm='ortho')`` is equal to
+    MATLAB ``dct(x)``.
+
+    For ``norm=None``, there is no scaling on `dct` and the `idct` is scaled by
+    ``1/N`` where ``N`` is the "logical" size of the DCT. For ``norm='ortho'``
+    both directions are scaled by the same factor ``1/sqrt(N)``.
+
+    There are theoretically 8 types of the DCT, only the first 4 types are
+    implemented in scipy. 'The' DCT generally refers to DCT type 2, and 'the'
+    Inverse DCT generally refers to DCT type 3.
+
+    **Type I**
+
+    There are several definitions of the DCT-I; we use the following
+    (for ``norm=None``)
+
+    .. math::
+
+       y_k = x_0 + (-1)^k x_{N-1} + 2 \sum_{n=1}^{N-2} x_n \cos\left(
+       \frac{\pi k n}{N-1} \right)
+
+    If ``norm='ortho'``, ``x[0]`` and ``x[N-1]`` are multiplied by a scaling
+    factor of :math:`\sqrt{2}`, and ``y[k]`` is multiplied by a scaling factor
+    ``f``
+
+    .. math::
+
+        f = \begin{cases}
+         \frac{1}{2}\sqrt{\frac{1}{N-1}} & \text{if }k=0\text{ or }N-1, \\
+         \frac{1}{2}\sqrt{\frac{2}{N-1}} & \text{otherwise} \end{cases}
+
+    .. note::
+       The DCT-I is only supported for input size > 1.
+
+    **Type II**
+
+    There are several definitions of the DCT-II; we use the following
+    (for ``norm=None``)
+
+    .. math::
+
+       y_k = 2 \sum_{n=0}^{N-1} x_n \cos\left(\frac{\pi k(2n+1)}{2N} \right)
+
+    If ``norm='ortho'``, ``y[k]`` is multiplied by a scaling factor ``f``
+
+    .. math::
+       f = \begin{cases}
+       \sqrt{\frac{1}{4N}} & \text{if }k=0, \\
+       \sqrt{\frac{1}{2N}} & \text{otherwise} \end{cases}
+
+    Which makes the corresponding matrix of coefficients orthonormal
+    (``O @ O.T = np.eye(N)``).
+
+    **Type III**
+
+    There are several definitions, we use the following (for ``norm=None``)
+
+    .. math::
+
+       y_k = x_0 + 2 \sum_{n=1}^{N-1} x_n \cos\left(\frac{\pi(2k+1)n}{2N}\right)
+
+    or, for ``norm='ortho'``
+
+    .. math::
+
+       y_k = \frac{x_0}{\sqrt{N}} + \sqrt{\frac{2}{N}} \sum_{n=1}^{N-1} x_n
+       \cos\left(\frac{\pi(2k+1)n}{2N}\right)
+
+    The (unnormalized) DCT-III is the inverse of the (unnormalized) DCT-II, up
+    to a factor `2N`. The orthonormalized DCT-III is exactly the inverse of
+    the orthonormalized DCT-II.
+
+    **Type IV**
+
+    There are several definitions of the DCT-IV; we use the following
+    (for ``norm=None``)
+
+    .. math::
+
+       y_k = 2 \sum_{n=0}^{N-1} x_n \cos\left(\frac{\pi(2k+1)(2n+1)}{4N} \right)
+
+    If ``norm='ortho'``, ``y[k]`` is multiplied by a scaling factor ``f``
+
+    .. math::
+
+        f = \frac{1}{\sqrt{2N}}
+
+    References
+    ----------
+    .. [1] 'A Fast Cosine Transform in One and Two Dimensions', by J.
+           Makhoul, `IEEE Transactions on acoustics, speech and signal
+           processing` vol. 28(1), pp. 27-34,
+           :doi:`10.1109/TASSP.1980.1163351` (1980).
+    .. [2] Wikipedia, "Discrete cosine transform",
+           https://en.wikipedia.org/wiki/Discrete_cosine_transform
+
+    Examples
+    --------
+    The Type 1 DCT is equivalent to the FFT (though faster) for real,
+    even-symmetrical inputs.  The output is also real and even-symmetrical.
+    Half of the FFT input is used to generate half of the FFT output:
+
+    >>> from scipy.fft import fft, dct
+    >>> fft(np.array([4., 3., 5., 10., 5., 3.])).real
+    array([ 30.,  -8.,   6.,  -2.,   6.,  -8.])
+    >>> dct(np.array([4., 3., 5., 10.]), 1)
+    array([ 30.,  -8.,   6.,  -2.])
+
+    """
     return (Dispatchable(x, np.ndarray),)
 
 
 @_dispatch
-@doc_replace(_fftpack.idct, 'fftpack', 'fft')
 def idct(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False):
+    """
+    Return the Inverse Discrete Cosine Transform of an arbitrary type sequence.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DCT (see Notes). Default type is 2.
+    n : int, optional
+        Length of the transform.  If ``n < x.shape[axis]``, `x` is
+        truncated.  If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the idct is computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    idct : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    dct : Forward DCT
+
+    Notes
+    -----
+    For a single dimension array `x`, ``idct(x, norm='ortho')`` is equal to
+    MATLAB ``idct(x)``.
+
+    'The' IDCT is the IDCT-II, which is the same as the normalized DCT-III.
+
+    The IDCT is equivalent to a normal DCT except for the normalization and
+    type. DCT type 1 and 4 are their own inverse and DCTs 2 and 3 are each
+    other's inverses.
+
+    Examples
+    --------
+    The Type 1 DCT is equivalent to the DFT for real, even-symmetrical
+    inputs.  The output is also real and even-symmetrical.  Half of the IFFT
+    input is used to generate half of the IFFT output:
+
+    >>> from scipy.fft import ifft, idct
+    >>> ifft(np.array([ 30.,  -8.,   6.,  -2.,   6.,  -8.])).real
+    array([  4.,   3.,   5.,  10.,   5.,   3.])
+    >>> idct(np.array([ 30.,  -8.,   6.,  -2.]), 1)
+    array([  4.,   3.,   5.,  10.])
+
+    """
     return (Dispatchable(x, np.ndarray),)
 
 
+def _idct(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False):
+    y = _fftpack.idct(x, type, n, axis, norm, overwrite_x)
+    if norm is None:
+        _normalize_inverse(y, 'c', type, axis)
+    return y
+
+
 @_dispatch
-@doc_replace(_fftpack.dst, 'fftpack', 'fft')
 def dst(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False):
+    r"""
+    Return the Discrete Sine Transform of arbitrary type sequence x.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DST (see Notes). Default type is 2.
+    n : int, optional
+        Length of the transform.  If ``n < x.shape[axis]``, `x` is
+        truncated.  If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the dst is computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    dst : ndarray of reals
+        The transformed input array.
+
+    See Also
+    --------
+    idst : Inverse DST
+
+    Notes
+    -----
+    For a single dimension array ``x``.
+
+    For ``norm=None``, there is no scaling on the `dst` and the `idst` is
+    scaled by ``1/N`` where ``N`` is the "logical" size of the DST. For
+    ``norm='ortho'`` both directions are scaled by the same factor
+    ``1/sqrt(N)``.
+
+    There are theoretically 8 types of the DST for different combinations of
+    even/odd boundary conditions and boundary off sets [1]_, only the first
+    4 types are implemented in scipy.
+
+    **Type I**
+
+    There are several definitions of the DST-I; we use the following
+    for ``norm=None``. DST-I assumes the input is odd around `n=-1` and `n=N`.
+
+    .. math::
+
+        y_k = 2 \sum_{n=0}^{N-1} x_n \sin\left(\frac{\pi(k+1)(n+1)}{N+1}\right)
+
+    Note that the DST-I is only supported for input size > 1.
+    The (unnormalized) DST-I is its own inverse, up to a factor `2(N+1)`.
+    The orthonormalized DST-I is exactly its own inverse.
+
+    **Type II**
+
+    There are several definitions of the DST-II; we use the following for
+    ``norm=None``. DST-II assumes the input is odd around `n=-1/2` and
+    `n=N-1/2`; the output is odd around :math:`k=-1` and even around `k=N-1`
+
+    .. math::
+
+        y_k = 2 \sum_{n=0}^{N-1} x_n \sin\left(\frac{\pi(k+1)(2n+1)}{2N}\right)
+
+    if ``norm='ortho'``, ``y[k]`` is multiplied by a scaling factor ``f``
+
+    .. math::
+
+        f = \begin{cases}
+        \sqrt{\frac{1}{4N}} & \text{if }k = 0, \\
+        \sqrt{\frac{1}{2N}} & \text{otherwise} \end{cases}
+
+    **Type III**
+
+    There are several definitions of the DST-III, we use the following (for
+    ``norm=None``). DST-III assumes the input is odd around `n=-1` and even
+    around `n=N-1`
+
+    .. math::
+
+        y_k = (-1)^k x_{N-1} + 2 \sum_{n=0}^{N-2} x_n \sin\left(
+        \frac{\pi(2k+1)(n+1)}{2N}\right)
+
+    The (unnormalized) DST-III is the inverse of the (unnormalized) DST-II, up
+    to a factor `2N`. The orthonormalized DST-III is exactly the inverse of the
+    orthonormalized DST-II.
+
+    **Type IV**
+
+    There are several definitions of the DST-IV, we use the following (for
+    ``norm=None``). DST-IV assumes the input is odd around `n=-0.5` and even
+    around `n=N-0.5`
+
+    .. math::
+
+        y_k = 2 \sum_{n=0}^{N-1} x_n \sin\left(\frac{\pi(2k+1)(2n+1)}{4N}\right)
+
+    The (unnormalized) DST-IV is its own inverse, up to a factor `2N`. The
+    orthonormalized DST-IV is exactly its own inverse.
+
+    References
+    ----------
+    .. [1] Wikipedia, "Discrete sine transform",
+           https://en.wikipedia.org/wiki/Discrete_sine_transform
+
+    """
     return (Dispatchable(x, np.ndarray),)
 
 
 @_dispatch
-@doc_replace(_fftpack.idst, 'fftpack', 'fft')
 def idst(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False):
+    """
+    Return the Inverse Discrete Sine Transform of an arbitrary type sequence.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DST (see Notes). Default type is 2.
+    n : int, optional
+        Length of the transform.  If ``n < x.shape[axis]``, `x` is
+        truncated.  If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the idst is computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    idst : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    dst : Forward DST
+
+    Notes
+    -----
+
+    'The' IDST is the IDST-II, which is the same as the normalized DST-III.
+
+    The IDST is equivalent to a normal DST except for the normalization and
+    type. DST type 1 and 4 are their own inverse and DSTs 2 and 3 are each
+    other's inverses.
+
+    """
     return (Dispatchable(x, np.ndarray),)
+
+
+def _idst(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False):
+    y = _fftpack.idst(x, type, n, axis, norm, overwrite_x)
+    if norm is None:
+        _normalize_inverse(y, 's', type, axis)
+    return y


Test patch:
diff --git a/scipy/fft/tests/test_backend.py b/scipy/fft/tests/test_backend.py
--- a/scipy/fft/tests/test_backend.py
+++ b/scipy/fft/tests/test_backend.py
@@ -2,6 +2,7 @@
 import scipy.fft
 from scipy.fft import set_backend
 from scipy.fft import _pocketfft
+from scipy.fft import _realtransforms as rt
 import scipy.fftpack as fftpack
 from scipy.fft.tests import mock_backend
 
@@ -21,8 +22,8 @@
             np.fft.irfft, np.fft.irfft2, np.fft.irfftn,
             np.fft.hfft, _pocketfft.hfft2, _pocketfft.hfftn,  # np has no hfftn
             np.fft.ihfft, _pocketfft.ihfft2, _pocketfft.ihfftn,
-            fftpack.dct, fftpack.idct, fftpack.dctn, fftpack.idctn,
-            fftpack.dst, fftpack.idst, fftpack.dstn, fftpack.idstn)
+            fftpack.dct, rt._idct, fftpack.dctn, rt._idctn,
+            fftpack.dst, rt._idst, fftpack.dstn, rt._idstn)
 
 funcs = (scipy.fft.fft, scipy.fft.fft2, scipy.fft.fftn,
          scipy.fft.ifft, scipy.fft.ifft2, scipy.fft.ifftn,
diff --git a/scipy/fft/tests/test_real_transforms.py b/scipy/fft/tests/test_real_transforms.py
new file mode 100644
--- /dev/null
+++ b/scipy/fft/tests/test_real_transforms.py
@@ -0,0 +1,87 @@
+
+import numpy as np
+from numpy.testing import assert_allclose
+import pytest
+
+from scipy.fft import dct, idct, dctn, idctn, dst, idst, dstn, idstn
+import scipy.fft as fft
+from scipy import fftpack
+
+# scipy.fft wraps the fftpack versions but with normalized inverse transforms.
+# So, the forward transforms and definitions are already thoroughly tested in
+# fftpack/test_real_transforms.py
+
+
+@pytest.mark.parametrize("forward, backward", [(dct, idct), (dst, idst)])
+@pytest.mark.parametrize("type", [1, 2, 3, 4])
+@pytest.mark.parametrize("n", [2, 3, 4, 5, 10, 16])
+@pytest.mark.parametrize("axis", [0, 1])
+@pytest.mark.parametrize("norm", [None, 'ortho'])
+def test_identity_1d(forward, backward, type, n, axis, norm):
+    # Test the identity f^-1(f(x)) == x
+    x = np.random.rand(n, n)
+
+    y = forward(x, type, axis=axis, norm=norm)
+    z = backward(y, type, axis=axis, norm=norm)
+    assert_allclose(z, x)
+
+    pad = [(0, 0)] * 2
+    pad[axis] = (0, 4)
+
+    y2 = np.pad(y, pad, mode='edge')
+    z2 = backward(y2, type, n, axis, norm)
+    assert_allclose(z2, x)
+
+
+@pytest.mark.parametrize("forward, backward", [(dctn, idctn), (dstn, idstn)])
+@pytest.mark.parametrize("type", [1, 2, 3, 4])
+@pytest.mark.parametrize("shape, axes",
+                         [
+                             ((4, 4), 0),
+                             ((4, 4), 1),
+                             ((4, 4), None),
+                             ((4, 4), (0, 1)),
+                             ((10, 12), None),
+                             ((10, 12), (0, 1)),
+                             ((4, 5, 6), None),
+                             ((4, 5, 6), 1),
+                             ((4, 5, 6), (0, 2)),
+                         ])
+@pytest.mark.parametrize("norm", [None, 'ortho'])
+def test_identity_nd(forward, backward, type, shape, axes, norm):
+    # Test the identity f^-1(f(x)) == x
+
+    x = np.random.random(shape)
+
+    if axes is not None:
+        shape = np.take(shape, axes)
+
+    y = forward(x, type, axes=axes, norm=norm)
+    z = backward(y, type, axes=axes, norm=norm)
+    assert_allclose(z, x)
+
+    if axes is None:
+        pad = [(0, 4)] * x.ndim
+    elif isinstance(axes, int):
+        pad = [(0, 0)] * x.ndim
+        pad[axes] = (0, 4)
+    else:
+        pad = [(0, 0)] * x.ndim
+
+        for a in axes:
+            pad[a] = (0, 4)
+
+    y2 = np.pad(y, pad, mode='edge')
+    z2 = backward(y2, type, shape, axes, norm)
+    assert_allclose(z2, x)
+
+
+@pytest.mark.parametrize("func", ['dct', 'dst', 'dctn', 'dstn'])
+@pytest.mark.parametrize("type", [1, 2, 3, 4])
+@pytest.mark.parametrize("norm", [None, 'ortho'])
+def test_fftpack_equivalience(func, type, norm):
+    x = np.random.rand(8, 16)
+    fft_res = getattr(fft, func)(x, type, norm=norm)
+    fftpack_res = getattr(fftpack, func)(x, type, norm=norm)
+
+    assert_allclose(fft_res, fftpack_res)
diff --git a/scipy/optimize/_trustregion_constr/tests/test_canonical_constraint.py b/scipy/optimize/_trustregion_constr/tests/test_canonical_constraint.py
--- a/scipy/optimize/_trustregion_constr/tests/test_canonical_constraint.py
+++ b/scipy/optimize/_trustregion_constr/tests/test_canonical_constraint.py
@@ -158,7 +158,7 @@ def test_nonlinear_constraint():
 def test_concatenation():
     rng = np.random.RandomState(0)
     n = 4
-    x0 = np.random.rand(n)
+    x0 = rng.rand(n)
 
     f1 = x0
     J1 = np.eye(n)