Repository: scipy/scipy

Issue:
MAINT:BLD:special:Overhaul _ufuncs and cython_special machinery
As discussed in #19824 

In the Cython file generation of special ufuncs and cython_special libs, comments below mention the rationale

https://github.com/scipy/scipy/blob/e8b043df1d586d9df9fed96add1a536859f1cb8c/scipy/special/_generate_pyx.py#L56-L68

many of the complications we have in the `special` build recipe is due to mixing Fortran and C sources. Now that the rewrite is stabilizing and fortran code is out. This machinery can probably be simplified a lot. 

A personal curiosity is whether we can get rid of the intermediate Cythonization step which unfortunately injects a lot of code and seemingly increases the binary size. 

I am not sure but I think scalar kernels can be linked to ufuncs directly without this C++/cython step


Discussion:
This is a very good point, as the SciPy special build is really far too over-complicated. It also connects with a thought I've been having that I want to pitch in here.

Given a scalar kernel, what is the easiest way to turn it into a NumPy ufunc? If we are just going to write C code, then we can use `PyUFunc_FromFuncAndDataAndSignature` directly. If we want to stick with Cython, then something like `@ufunc` would do it quite easily. See https://cython.readthedocs.io/en/latest/src/userguide/numpy_ufuncs.html

The only thing is `@ufunc` doesn't support gufuncs at the moment, but I suppose that could be added. In some of the other array libraries, like CuPy, gufuncs can be created dynamically to wrap C++ or CUDA code. See https://docs.cupy.dev/en/stable/reference/generated/cupy.ElementwiseKernel.html

Is there anything equivalent for NumPy? What would SciPy like to use for this long term?

I personally have used `PyUFunc_FromFuncAndDataAndSignature` from the NumPy C API to turn C++ kernels into ufuncs, but I'd be keen to use something like `@ufunc` from Cython if it supported everything I needed.

Resolving patch:
diff --git a/scipy/special/_add_newdocs.py b/scipy/special/_add_newdocs.py
--- a/scipy/special/_add_newdocs.py
+++ b/scipy/special/_add_newdocs.py
@@ -1007,168 +1007,6 @@ def add_newdoc(name, doc):
 
     """)
 
-add_newdoc("bei",
-    r"""
-    bei(x, out=None)
-
-    Kelvin function bei.
-
-    Defined as
-
-    .. math::
-
-        \mathrm{bei}(x) = \Im[J_0(x e^{3 \pi i / 4})]
-
-    where :math:`J_0` is the Bessel function of the first kind of
-    order zero (see `jv`). See [dlmf]_ for more details.
-
-    Parameters
-    ----------
-    x : array_like
-        Real argument.
-    out : ndarray, optional
-        Optional output array for the function results.
-
-    Returns
-    -------
-    scalar or ndarray
-        Values of the Kelvin function.
-
-    See Also
-    --------
-    ber : the corresponding real part
-    beip : the derivative of bei
-    jv : Bessel function of the first kind
-
-    References
-    ----------
-    .. [dlmf] NIST, Digital Library of Mathematical Functions,
-        https://dlmf.nist.gov/10.61
-
-    Examples
-    --------
-    It can be expressed using Bessel functions.
-
-    >>> import numpy as np
-    >>> import scipy.special as sc
-    >>> x = np.array([1.0, 2.0, 3.0, 4.0])
-    >>> sc.jv(0, x * np.exp(3 * np.pi * 1j / 4)).imag
-    array([0.24956604, 0.97229163, 1.93758679, 2.29269032])
-    >>> sc.bei(x)
-    array([0.24956604, 0.97229163, 1.93758679, 2.29269032])
-
-    """)
-
-add_newdoc("beip",
-    r"""
-    beip(x, out=None)
-
-    Derivative of the Kelvin function bei.
-
-    Parameters
-    ----------
-    x : array_like
-        Real argument.
-    out : ndarray, optional
-        Optional output array for the function results.
-
-    Returns
-    -------
-    scalar or ndarray
-        The values of the derivative of bei.
-
-    See Also
-    --------
-    bei
-
-    References
-    ----------
-    .. [dlmf] NIST, Digital Library of Mathematical Functions,
-        https://dlmf.nist.gov/10#PT5
-
-    """)
-
-add_newdoc("ber",
-    r"""
-    ber(x, out=None)
-
-    Kelvin function ber.
-
-    Defined as
-
-    .. math::
-
-        \mathrm{ber}(x) = \Re[J_0(x e^{3 \pi i / 4})]
-
-    where :math:`J_0` is the Bessel function of the first kind of
-    order zero (see `jv`). See [dlmf]_ for more details.
-
-    Parameters
-    ----------
-    x : array_like
-        Real argument.
-    out : ndarray, optional
-        Optional output array for the function results.
-
-    Returns
-    -------
-    scalar or ndarray
-        Values of the Kelvin function.
-
-    See Also
-    --------
-    bei : the corresponding real part
-    berp : the derivative of bei
-    jv : Bessel function of the first kind
-
-    References
-    ----------
-    .. [dlmf] NIST, Digital Library of Mathematical Functions,
-        https://dlmf.nist.gov/10.61
-
-    Examples
-    --------
-    It can be expressed using Bessel functions.
-
-    >>> import numpy as np
-    >>> import scipy.special as sc
-    >>> x = np.array([1.0, 2.0, 3.0, 4.0])
-    >>> sc.jv(0, x * np.exp(3 * np.pi * 1j / 4)).real
-    array([ 0.98438178,  0.75173418, -0.22138025, -2.56341656])
-    >>> sc.ber(x)
-    array([ 0.98438178,  0.75173418, -0.22138025, -2.56341656])
-
-    """)
-
-add_newdoc("berp",
-    r"""
-    berp(x, out=None)
-
-    Derivative of the Kelvin function ber.
-
-    Parameters
-    ----------
-    x : array_like
-        Real argument.
-    out : ndarray, optional
-        Optional output array for the function results.
-
-    Returns
-    -------
-    scalar or ndarray
-        The values of the derivative of ber.
-
-    See Also
-    --------
-    ber
-
-    References
-    ----------
-    .. [dlmf] NIST, Digital Library of Mathematical Functions,
-        https://dlmf.nist.gov/10#PT5
-
-    """)
-
 add_newdoc("besselpoly",
     r"""
     besselpoly(a, lmb, nu, out=None)
@@ -4522,88 +4360,6 @@ def add_newdoc(name, doc):
 
     """)
 
-add_newdoc("exp1",
-    r"""
-    exp1(z, out=None)
-
-    Exponential integral E1.
-
-    For complex :math:`z \ne 0` the exponential integral can be defined as
-    [1]_
-
-    .. math::
-
-       E_1(z) = \int_z^\infty \frac{e^{-t}}{t} dt,
-
-    where the path of the integral does not cross the negative real
-    axis or pass through the origin.
-
-    Parameters
-    ----------
-    z: array_like
-        Real or complex argument.
-    out : ndarray, optional
-        Optional output array for the function results
-
-    Returns
-    -------
-    scalar or ndarray
-        Values of the exponential integral E1
-
-    See Also
-    --------
-    expi : exponential integral :math:`Ei`
-    expn : generalization of :math:`E_1`
-
-    Notes
-    -----
-    For :math:`x > 0` it is related to the exponential integral
-    :math:`Ei` (see `expi`) via the relation
-
-    .. math::
-
-       E_1(x) = -Ei(-x).
-
-    References
-    ----------
-    .. [1] Digital Library of Mathematical Functions, 6.2.1
-           https://dlmf.nist.gov/6.2#E1
-
-    Examples
-    --------
-    >>> import numpy as np
-    >>> import scipy.special as sc
-
-    It has a pole at 0.
-
-    >>> sc.exp1(0)
-    inf
-
-    It has a branch cut on the negative real axis.
-
-    >>> sc.exp1(-1)
-    nan
-    >>> sc.exp1(complex(-1, 0))
-    (-1.8951178163559368-3.141592653589793j)
-    >>> sc.exp1(complex(-1, -0.0))
-    (-1.8951178163559368+3.141592653589793j)
-
-    It approaches 0 along the positive real axis.
-
-    >>> sc.exp1([1, 10, 100, 1000])
-    array([2.19383934e-01, 4.15696893e-06, 3.68359776e-46, 0.00000000e+00])
-
-    It is related to `expi`.
-
-    >>> x = np.array([1, 2, 3, 4])
-    >>> sc.exp1(x)
-    array([0.21938393, 0.04890051, 0.01304838, 0.00377935])
-    >>> -sc.expi(-x)
-    array([0.21938393, 0.04890051, 0.01304838, 0.00377935])
-
-    """)
-
-
 add_newdoc(
     "_scaled_exp1",
     """
@@ -4721,94 +4477,6 @@ def add_newdoc(name, doc):
            [ 1.41421356,  2.        ,  2.82842712]])
     """)
 
-add_newdoc("expi",
-    r"""
-    expi(x, out=None)
-
-    Exponential integral Ei.
-
-    For real :math:`x`, the exponential integral is defined as [1]_
-
-    .. math::
-
-        Ei(x) = \int_{-\infty}^x \frac{e^t}{t} dt.
-
-    For :math:`x > 0` the integral is understood as a Cauchy principal
-    value.
-
-    It is extended to the complex plane by analytic continuation of
-    the function on the interval :math:`(0, \infty)`. The complex
-    variant has a branch cut on the negative real axis.
-
-    Parameters
-    ----------
-    x : array_like
-        Real or complex valued argument
-    out : ndarray, optional
-        Optional output array for the function results
-
-    Returns
-    -------
-    scalar or ndarray
-        Values of the exponential integral
-
-    See Also
-    --------
-    exp1 : Exponential integral :math:`E_1`
-    expn : Generalized exponential integral :math:`E_n`
-
-    Notes
-    -----
-    The exponential integrals :math:`E_1` and :math:`Ei` satisfy the
-    relation
-
-    .. math::
-
-        E_1(x) = -Ei(-x)
-
-    for :math:`x > 0`.
-
-    References
-    ----------
-    .. [1] Digital Library of Mathematical Functions, 6.2.5
-           https://dlmf.nist.gov/6.2#E5
-
-    Examples
-    --------
-    >>> import numpy as np
-    >>> import scipy.special as sc
-
-    It is related to `exp1`.
-
-    >>> x = np.array([1, 2, 3, 4])
-    >>> -sc.expi(-x)
-    array([0.21938393, 0.04890051, 0.01304838, 0.00377935])
-    >>> sc.exp1(x)
-    array([0.21938393, 0.04890051, 0.01304838, 0.00377935])
-
-    The complex variant has a branch cut on the negative real axis.
-
-    >>> sc.expi(-1 + 1e-12j)
-    (-0.21938393439552062+3.1415926535894254j)
-    >>> sc.expi(-1 - 1e-12j)
-    (-0.21938393439552062-3.1415926535894254j)
-
-    As the complex variant approaches the branch cut, the real parts
-    approach the value of the real variant.
-
-    >>> sc.expi(-1)
-    -0.21938393439552062
-
-    The SciPy implementation returns the real variant for complex
-    values on the branch cut.
-
-    >>> sc.expi(complex(-1, 0.0))
-    (-0.21938393439552062-0j)
-    >>> sc.expi(complex(-1, -0.0))
-    (-0.21938393439552062-0j)
-
-    """)
-
 add_newdoc('expit',
     """
     expit(x, out=None)
@@ -5853,137 +5521,63 @@ def add_newdoc(name, doc):
 
     """)
 
-add_newdoc("gammaln",
+add_newdoc("gammasgn",
     r"""
-    gammaln(x, out=None)
+    gammasgn(x, out=None)
 
-    Logarithm of the absolute value of the gamma function.
+    Sign of the gamma function.
 
-    Defined as
+    It is defined as
 
     .. math::
 
-       \ln(\lvert\Gamma(x)\rvert)
+       \text{gammasgn}(x) =
+       \begin{cases}
+         +1 & \Gamma(x) > 0 \\
+         -1 & \Gamma(x) < 0
+       \end{cases}
 
-    where :math:`\Gamma` is the gamma function. For more details on
-    the gamma function, see [dlmf]_.
+    where :math:`\Gamma` is the gamma function; see `gamma`. This
+    definition is complete since the gamma function is never zero;
+    see the discussion after [dlmf]_.
 
     Parameters
     ----------
     x : array_like
         Real argument
     out : ndarray, optional
-        Optional output array for the function results
+        Optional output array for the function values
 
     Returns
     -------
     scalar or ndarray
-        Values of the log of the absolute value of gamma
+        Sign of the gamma function
 
     See Also
     --------
-    gammasgn : sign of the gamma function
-    loggamma : principal branch of the logarithm of the gamma function
+    gamma : the gamma function
+    gammaln : log of the absolute value of the gamma function
+    loggamma : analytic continuation of the log of the gamma function
 
     Notes
     -----
-    It is the same function as the Python standard library function
-    :func:`math.lgamma`.
-
-    When used in conjunction with `gammasgn`, this function is useful
-    for working in logspace on the real axis without having to deal
-    with complex numbers via the relation ``exp(gammaln(x)) =
-    gammasgn(x) * gamma(x)``.
-
-    For complex-valued log-gamma, use `loggamma` instead of `gammaln`.
+    The gamma function can be computed as ``gammasgn(x) *
+    np.exp(gammaln(x))``.
 
     References
     ----------
     .. [dlmf] NIST Digital Library of Mathematical Functions
-              https://dlmf.nist.gov/5
+              https://dlmf.nist.gov/5.2#E1
 
     Examples
     --------
     >>> import numpy as np
     >>> import scipy.special as sc
 
-    It has two positive zeros.
+    It is 1 for `x > 0`.
 
-    >>> sc.gammaln([1, 2])
-    array([0., 0.])
-
-    It has poles at nonpositive integers.
-
-    >>> sc.gammaln([0, -1, -2, -3, -4])
-    array([inf, inf, inf, inf, inf])
-
-    It asymptotically approaches ``x * log(x)`` (Stirling's formula).
-
-    >>> x = np.array([1e10, 1e20, 1e40, 1e80])
-    >>> sc.gammaln(x)
-    array([2.20258509e+11, 4.50517019e+21, 9.11034037e+41, 1.83206807e+82])
-    >>> x * np.log(x)
-    array([2.30258509e+11, 4.60517019e+21, 9.21034037e+41, 1.84206807e+82])
-
-    """)
-
-add_newdoc("gammasgn",
-    r"""
-    gammasgn(x, out=None)
-
-    Sign of the gamma function.
-
-    It is defined as
-
-    .. math::
-
-       \text{gammasgn}(x) =
-       \begin{cases}
-         +1 & \Gamma(x) > 0 \\
-         -1 & \Gamma(x) < 0
-       \end{cases}
-
-    where :math:`\Gamma` is the gamma function; see `gamma`. This
-    definition is complete since the gamma function is never zero;
-    see the discussion after [dlmf]_.
-
-    Parameters
-    ----------
-    x : array_like
-        Real argument
-    out : ndarray, optional
-        Optional output array for the function values
-
-    Returns
-    -------
-    scalar or ndarray
-        Sign of the gamma function
-
-    See Also
-    --------
-    gamma : the gamma function
-    gammaln : log of the absolute value of the gamma function
-    loggamma : analytic continuation of the log of the gamma function
-
-    Notes
-    -----
-    The gamma function can be computed as ``gammasgn(x) *
-    np.exp(gammaln(x))``.
-
-    References
-    ----------
-    .. [dlmf] NIST Digital Library of Mathematical Functions
-              https://dlmf.nist.gov/5.2#E1
-
-    Examples
-    --------
-    >>> import numpy as np
-    >>> import scipy.special as sc
-
-    It is 1 for `x > 0`.
-
-    >>> sc.gammasgn([1, 2, 3, 4])
-    array([1., 1., 1., 1.])
+    >>> sc.gammasgn([1, 2, 3, 4])
+    array([1., 1., 1., 1.])
 
     It alternates between -1 and 1 for negative integers.
 
@@ -7332,538 +6926,6 @@ def add_newdoc(name, doc):
     Internal function, do not use.
     """)
 
-add_newdoc("it2i0k0",
-    r"""
-    it2i0k0(x, out=None)
-
-    Integrals related to modified Bessel functions of order 0.
-
-    Computes the integrals
-
-    .. math::
-
-        \int_0^x \frac{I_0(t) - 1}{t} dt \\
-        \int_x^\infty \frac{K_0(t)}{t} dt.
-
-    Parameters
-    ----------
-    x : array_like
-        Values at which to evaluate the integrals.
-    out : tuple of ndarrays, optional
-        Optional output arrays for the function results.
-
-    Returns
-    -------
-    ii0 : scalar or ndarray
-        The integral for `i0`
-    ik0 : scalar or ndarray
-        The integral for `k0`
-
-    References
-    ----------
-    .. [1] S. Zhang and J.M. Jin, "Computation of Special Functions",
-           Wiley 1996
-
-    Examples
-    --------
-    Evaluate the functions at one point.
-
-    >>> from scipy.special import it2i0k0
-    >>> int_i, int_k = it2i0k0(1.)
-    >>> int_i, int_k
-    (0.12897944249456852, 0.2085182909001295)
-
-    Evaluate the functions at several points.
-
-    >>> import numpy as np
-    >>> points = np.array([0.5, 1.5, 3.])
-    >>> int_i, int_k = it2i0k0(points)
-    >>> int_i, int_k
-    (array([0.03149527, 0.30187149, 1.50012461]),
-     array([0.66575102, 0.0823715 , 0.00823631]))
-
-    Plot the functions from 0 to 5.
-
-    >>> import matplotlib.pyplot as plt
-    >>> fig, ax = plt.subplots()
-    >>> x = np.linspace(0., 5., 1000)
-    >>> int_i, int_k = it2i0k0(x)
-    >>> ax.plot(x, int_i, label=r"$\int_0^x \frac{I_0(t)-1}{t}\,dt$")
-    >>> ax.plot(x, int_k, label=r"$\int_x^{\infty} \frac{K_0(t)}{t}\,dt$")
-    >>> ax.legend()
-    >>> ax.set_ylim(0, 10)
-    >>> plt.show()
-    """)
-
-add_newdoc("it2j0y0",
-    r"""
-    it2j0y0(x, out=None)
-
-    Integrals related to Bessel functions of the first kind of order 0.
-
-    Computes the integrals
-
-    .. math::
-
-        \int_0^x \frac{1 - J_0(t)}{t} dt \\
-        \int_x^\infty \frac{Y_0(t)}{t} dt.
-
-    For more on :math:`J_0` and :math:`Y_0` see `j0` and `y0`.
-
-    Parameters
-    ----------
-    x : array_like
-        Values at which to evaluate the integrals.
-    out : tuple of ndarrays, optional
-        Optional output arrays for the function results.
-
-    Returns
-    -------
-    ij0 : scalar or ndarray
-        The integral for `j0`
-    iy0 : scalar or ndarray
-        The integral for `y0`
-
-    References
-    ----------
-    .. [1] S. Zhang and J.M. Jin, "Computation of Special Functions",
-           Wiley 1996
-
-    Examples
-    --------
-    Evaluate the functions at one point.
-
-    >>> from scipy.special import it2j0y0
-    >>> int_j, int_y = it2j0y0(1.)
-    >>> int_j, int_y
-    (0.12116524699506871, 0.39527290169929336)
-
-    Evaluate the functions at several points.
-
-    >>> import numpy as np
-    >>> points = np.array([0.5, 1.5, 3.])
-    >>> int_j, int_y = it2j0y0(points)
-    >>> int_j, int_y
-    (array([0.03100699, 0.26227724, 0.85614669]),
-     array([ 0.26968854,  0.29769696, -0.02987272]))
-
-    Plot the functions from 0 to 10.
-
-    >>> import matplotlib.pyplot as plt
-    >>> fig, ax = plt.subplots()
-    >>> x = np.linspace(0., 10., 1000)
-    >>> int_j, int_y = it2j0y0(x)
-    >>> ax.plot(x, int_j, label=r"$\int_0^x \frac{1-J_0(t)}{t}\,dt$")
-    >>> ax.plot(x, int_y, label=r"$\int_x^{\infty} \frac{Y_0(t)}{t}\,dt$")
-    >>> ax.legend()
-    >>> ax.set_ylim(-2.5, 2.5)
-    >>> plt.show()
-    """)
-
-add_newdoc("it2struve0",
-    r"""
-    it2struve0(x, out=None)
-
-    Integral related to the Struve function of order 0.
-
-    Returns the integral,
-
-    .. math::
-        \int_x^\infty \frac{H_0(t)}{t}\,dt
-
-    where :math:`H_0` is the Struve function of order 0.
-
-    Parameters
-    ----------
-    x : array_like
-        Lower limit of integration.
-    out : ndarray, optional
-        Optional output array for the function values
-
-    Returns
-    -------
-    I : scalar or ndarray
-        The value of the integral.
-
-    See Also
-    --------
-    struve
-
-    Notes
-    -----
-    Wrapper for a Fortran routine created by Shanjie Zhang and Jianming
-    Jin [1]_.
-
-    References
-    ----------
-    .. [1] Zhang, Shanjie and Jin, Jianming. "Computation of Special
-           Functions", John Wiley and Sons, 1996.
-           https://people.sc.fsu.edu/~jburkardt/f_src/special_functions/special_functions.html
-
-    Examples
-    --------
-    Evaluate the function at one point.
-
-    >>> import numpy as np
-    >>> from scipy.special import it2struve0
-    >>> it2struve0(1.)
-    0.9571973506383524
-
-    Evaluate the function at several points by supplying
-    an array for `x`.
-
-    >>> points = np.array([1., 2., 3.5])
-    >>> it2struve0(points)
-    array([0.95719735, 0.46909296, 0.10366042])
-
-    Plot the function from -10 to 10.
-
-    >>> import matplotlib.pyplot as plt
-    >>> x = np.linspace(-10., 10., 1000)
-    >>> it2struve0_values = it2struve0(x)
-    >>> fig, ax = plt.subplots()
-    >>> ax.plot(x, it2struve0_values)
-    >>> ax.set_xlabel(r'$x$')
-    >>> ax.set_ylabel(r'$\int_x^{\infty}\frac{H_0(t)}{t}\,dt$')
-    >>> plt.show()
-    """)
-
-add_newdoc(
-    "itairy",
-    r"""
-    itairy(x, out=None)
-
-    Integrals of Airy functions
-
-    Calculates the integrals of Airy functions from 0 to `x`.
-
-    Parameters
-    ----------
-
-    x : array_like
-        Upper limit of integration (float).
-    out : tuple of ndarray, optional
-        Optional output arrays for the function values
-
-    Returns
-    -------
-    Apt : scalar or ndarray
-        Integral of Ai(t) from 0 to x.
-    Bpt : scalar or ndarray
-        Integral of Bi(t) from 0 to x.
-    Ant : scalar or ndarray
-        Integral of Ai(-t) from 0 to x.
-    Bnt : scalar or ndarray
-        Integral of Bi(-t) from 0 to x.
-
-    Notes
-    -----
-
-    Wrapper for a Fortran routine created by Shanjie Zhang and Jianming
-    Jin [1]_.
-
-    References
-    ----------
-
-    .. [1] Zhang, Shanjie and Jin, Jianming. "Computation of Special
-           Functions", John Wiley and Sons, 1996.
-           https://people.sc.fsu.edu/~jburkardt/f_src/special_functions/special_functions.html
-
-    Examples
-    --------
-    Compute the functions at ``x=1.``.
-
-    >>> import numpy as np
-    >>> from scipy.special import itairy
-    >>> import matplotlib.pyplot as plt
-    >>> apt, bpt, ant, bnt = itairy(1.)
-    >>> apt, bpt, ant, bnt
-    (0.23631734191710949,
-     0.8727691167380077,
-     0.46567398346706845,
-     0.3730050096342943)
-
-    Compute the functions at several points by providing a NumPy array for `x`.
-
-    >>> x = np.array([1., 1.5, 2.5, 5])
-    >>> apt, bpt, ant, bnt = itairy(x)
-    >>> apt, bpt, ant, bnt
-    (array([0.23631734, 0.28678675, 0.324638  , 0.33328759]),
-     array([  0.87276912,   1.62470809,   5.20906691, 321.47831857]),
-     array([0.46567398, 0.72232876, 0.93187776, 0.7178822 ]),
-     array([ 0.37300501,  0.35038814, -0.02812939,  0.15873094]))
-
-    Plot the functions from -10 to 10.
-
-    >>> x = np.linspace(-10, 10, 500)
-    >>> apt, bpt, ant, bnt = itairy(x)
-    >>> fig, ax = plt.subplots(figsize=(6, 5))
-    >>> ax.plot(x, apt, label=r"$\int_0^x\, Ai(t)\, dt$")
-    >>> ax.plot(x, bpt, ls="dashed", label=r"$\int_0^x\, Bi(t)\, dt$")
-    >>> ax.plot(x, ant, ls="dashdot", label=r"$\int_0^x\, Ai(-t)\, dt$")
-    >>> ax.plot(x, bnt, ls="dotted", label=r"$\int_0^x\, Bi(-t)\, dt$")
-    >>> ax.set_ylim(-2, 1.5)
-    >>> ax.legend(loc="lower right")
-    >>> plt.show()
-    """)
-
-add_newdoc("iti0k0",
-    r"""
-    iti0k0(x, out=None)
-
-    Integrals of modified Bessel functions of order 0.
-
-    Computes the integrals
-
-    .. math::
-
-        \int_0^x I_0(t) dt \\
-        \int_0^x K_0(t) dt.
-
-    For more on :math:`I_0` and :math:`K_0` see `i0` and `k0`.
-
-    Parameters
-    ----------
-    x : array_like
-        Values at which to evaluate the integrals.
-    out : tuple of ndarrays, optional
-        Optional output arrays for the function results.
-
-    Returns
-    -------
-    ii0 : scalar or ndarray
-        The integral for `i0`
-    ik0 : scalar or ndarray
-        The integral for `k0`
-
-    References
-    ----------
-    .. [1] S. Zhang and J.M. Jin, "Computation of Special Functions",
-           Wiley 1996
-
-    Examples
-    --------
-    Evaluate the functions at one point.
-
-    >>> from scipy.special import iti0k0
-    >>> int_i, int_k = iti0k0(1.)
-    >>> int_i, int_k
-    (1.0865210970235892, 1.2425098486237771)
-
-    Evaluate the functions at several points.
-
-    >>> import numpy as np
-    >>> points = np.array([0., 1.5, 3.])
-    >>> int_i, int_k = iti0k0(points)
-    >>> int_i, int_k
-    (array([0.        , 1.80606937, 6.16096149]),
-     array([0.        , 1.39458246, 1.53994809]))
-
-    Plot the functions from 0 to 5.
-
-    >>> import matplotlib.pyplot as plt
-    >>> fig, ax = plt.subplots()
-    >>> x = np.linspace(0., 5., 1000)
-    >>> int_i, int_k = iti0k0(x)
-    >>> ax.plot(x, int_i, label=r"$\int_0^x I_0(t)\,dt$")
-    >>> ax.plot(x, int_k, label=r"$\int_0^x K_0(t)\,dt$")
-    >>> ax.legend()
-    >>> plt.show()
-    """)
-
-add_newdoc("itj0y0",
-    r"""
-    itj0y0(x, out=None)
-
-    Integrals of Bessel functions of the first kind of order 0.
-
-    Computes the integrals
-
-    .. math::
-
-        \int_0^x J_0(t) dt \\
-        \int_0^x Y_0(t) dt.
-
-    For more on :math:`J_0` and :math:`Y_0` see `j0` and `y0`.
-
-    Parameters
-    ----------
-    x : array_like
-        Values at which to evaluate the integrals.
-    out : tuple of ndarrays, optional
-        Optional output arrays for the function results.
-
-    Returns
-    -------
-    ij0 : scalar or ndarray
-        The integral of `j0`
-    iy0 : scalar or ndarray
-        The integral of `y0`
-
-    References
-    ----------
-    .. [1] S. Zhang and J.M. Jin, "Computation of Special Functions",
-           Wiley 1996
-
-    Examples
-    --------
-    Evaluate the functions at one point.
-
-    >>> from scipy.special import itj0y0
-    >>> int_j, int_y = itj0y0(1.)
-    >>> int_j, int_y
-    (0.9197304100897596, -0.637069376607422)
-
-    Evaluate the functions at several points.
-
-    >>> import numpy as np
-    >>> points = np.array([0., 1.5, 3.])
-    >>> int_j, int_y = itj0y0(points)
-    >>> int_j, int_y
-    (array([0.        , 1.24144951, 1.38756725]),
-     array([ 0.        , -0.51175903,  0.19765826]))
-
-    Plot the functions from 0 to 10.
-
-    >>> import matplotlib.pyplot as plt
-    >>> fig, ax = plt.subplots()
-    >>> x = np.linspace(0., 10., 1000)
-    >>> int_j, int_y = itj0y0(x)
-    >>> ax.plot(x, int_j, label=r"$\int_0^x J_0(t)\,dt$")
-    >>> ax.plot(x, int_y, label=r"$\int_0^x Y_0(t)\,dt$")
-    >>> ax.legend()
-    >>> plt.show()
-
-    """)
-
-add_newdoc("itmodstruve0",
-    r"""
-    itmodstruve0(x, out=None)
-
-    Integral of the modified Struve function of order 0.
-
-    .. math::
-        I = \int_0^x L_0(t)\,dt
-
-    Parameters
-    ----------
-    x : array_like
-        Upper limit of integration (float).
-    out : ndarray, optional
-        Optional output array for the function values
-
-    Returns
-    -------
-    I : scalar or ndarray
-        The integral of :math:`L_0` from 0 to `x`.
-
-    See Also
-    --------
-    modstruve: Modified Struve function which is integrated by this function
-
-    Notes
-    -----
-    Wrapper for a Fortran routine created by Shanjie Zhang and Jianming
-    Jin [1]_.
-
-    References
-    ----------
-    .. [1] Zhang, Shanjie and Jin, Jianming. "Computation of Special
-           Functions", John Wiley and Sons, 1996.
-           https://people.sc.fsu.edu/~jburkardt/f_src/special_functions/special_functions.html
-
-    Examples
-    --------
-    Evaluate the function at one point.
-
-    >>> import numpy as np
-    >>> from scipy.special import itmodstruve0
-    >>> itmodstruve0(1.)
-    0.3364726286440384
-
-    Evaluate the function at several points by supplying
-    an array for `x`.
-
-    >>> points = np.array([1., 2., 3.5])
-    >>> itmodstruve0(points)
-    array([0.33647263, 1.588285  , 7.60382578])
-
-    Plot the function from -10 to 10.
-
-    >>> import matplotlib.pyplot as plt
-    >>> x = np.linspace(-10., 10., 1000)
-    >>> itmodstruve0_values = itmodstruve0(x)
-    >>> fig, ax = plt.subplots()
-    >>> ax.plot(x, itmodstruve0_values)
-    >>> ax.set_xlabel(r'$x$')
-    >>> ax.set_ylabel(r'$\int_0^xL_0(t)\,dt$')
-    >>> plt.show()
-    """)
-
-add_newdoc("itstruve0",
-    r"""
-    itstruve0(x, out=None)
-
-    Integral of the Struve function of order 0.
-
-    .. math::
-        I = \int_0^x H_0(t)\,dt
-
-    Parameters
-    ----------
-    x : array_like
-        Upper limit of integration (float).
-    out : ndarray, optional
-        Optional output array for the function values
-
-    Returns
-    -------
-    I : scalar or ndarray
-        The integral of :math:`H_0` from 0 to `x`.
-
-    See Also
-    --------
-    struve: Function which is integrated by this function
-
-    Notes
-    -----
-    Wrapper for a Fortran routine created by Shanjie Zhang and Jianming
-    Jin [1]_.
-
-    References
-    ----------
-    .. [1] Zhang, Shanjie and Jin, Jianming. "Computation of Special
-           Functions", John Wiley and Sons, 1996.
-           https://people.sc.fsu.edu/~jburkardt/f_src/special_functions/special_functions.html
-
-    Examples
-    --------
-    Evaluate the function at one point.
-
-    >>> import numpy as np
-    >>> from scipy.special import itstruve0
-    >>> itstruve0(1.)
-    0.30109042670805547
-
-    Evaluate the function at several points by supplying
-    an array for `x`.
-
-    >>> points = np.array([1., 2., 3.5])
-    >>> itstruve0(points)
-    array([0.30109043, 1.01870116, 1.96804581])
-
-    Plot the function from -20 to 20.
-
-    >>> import matplotlib.pyplot as plt
-    >>> x = np.linspace(-20., 20., 1000)
-    >>> istruve0_values = itstruve0(x)
-    >>> fig, ax = plt.subplots()
-    >>> ax.plot(x, istruve0_values)
-    >>> ax.set_xlabel(r'$x$')
-    >>> ax.set_ylabel(r'$\int_0^{x}H_0(t)\,dt$')
-    >>> plt.show()
-    """)
-
 add_newdoc("iv",
     r"""
     iv(v, z, out=None)
@@ -8680,131 +7742,49 @@ def add_newdoc(name, doc):
     -------
     K : scalar or ndarray
         Value of the exponentially scaled modified Bessel function K of order
-        1 at `x`.
-
-    See Also
-    --------
-    kv: Modified Bessel function of the second kind of any order
-    k1: Modified Bessel function of the second kind of order 1
-
-    Notes
-    -----
-    The range is partitioned into the two intervals [0, 2] and (2, infinity).
-    Chebyshev polynomial expansions are employed in each interval.
-
-    This function is a wrapper for the Cephes [1]_ routine `k1e`.
-
-    References
-    ----------
-    .. [1] Cephes Mathematical Functions Library,
-           http://www.netlib.org/cephes/
-
-    Examples
-    --------
-    In the following example `k1` returns 0 whereas `k1e` still returns a
-    useful floating point number.
-
-    >>> from scipy.special import k1, k1e
-    >>> k1(1000.), k1e(1000.)
-    (0., 0.03964813081296021)
-
-    Calculate the function at several points by providing a NumPy array or
-    list for `x`:
-
-    >>> import numpy as np
-    >>> k1e(np.array([0.5, 2., 3.]))
-    array([2.73100971, 1.03347685, 0.80656348])
-
-    Plot the function from 0 to 10.
-
-    >>> import matplotlib.pyplot as plt
-    >>> fig, ax = plt.subplots()
-    >>> x = np.linspace(0., 10., 1000)
-    >>> y = k1e(x)
-    >>> ax.plot(x, y)
-    >>> plt.show()
-    """)
-
-add_newdoc("kei",
-    r"""
-    kei(x, out=None)
-
-    Kelvin function kei.
-
-    Defined as
-
-    .. math::
-
-        \mathrm{kei}(x) = \Im[K_0(x e^{\pi i / 4})]
-
-    where :math:`K_0` is the modified Bessel function of the second
-    kind (see `kv`). See [dlmf]_ for more details.
-
-    Parameters
-    ----------
-    x : array_like
-        Real argument.
-    out : ndarray, optional
-        Optional output array for the function results.
-
-    Returns
-    -------
-    scalar or ndarray
-        Values of the Kelvin function.
-
-    See Also
-    --------
-    ker : the corresponding real part
-    keip : the derivative of kei
-    kv : modified Bessel function of the second kind
-
-    References
-    ----------
-    .. [dlmf] NIST, Digital Library of Mathematical Functions,
-        https://dlmf.nist.gov/10.61
-
-    Examples
-    --------
-    It can be expressed using the modified Bessel function of the
-    second kind.
-
-    >>> import numpy as np
-    >>> import scipy.special as sc
-    >>> x = np.array([1.0, 2.0, 3.0, 4.0])
-    >>> sc.kv(0, x * np.exp(np.pi * 1j / 4)).imag
-    array([-0.49499464, -0.20240007, -0.05112188,  0.0021984 ])
-    >>> sc.kei(x)
-    array([-0.49499464, -0.20240007, -0.05112188,  0.0021984 ])
-
-    """)
-
-add_newdoc("keip",
-    r"""
-    keip(x, out=None)
-
-    Derivative of the Kelvin function kei.
-
-    Parameters
-    ----------
-    x : array_like
-        Real argument.
-    out : ndarray, optional
-        Optional output array for the function results.
-
-    Returns
-    -------
-    scalar or ndarray
-        The values of the derivative of kei.
+        1 at `x`.
 
     See Also
     --------
-    kei
+    kv: Modified Bessel function of the second kind of any order
+    k1: Modified Bessel function of the second kind of order 1
+
+    Notes
+    -----
+    The range is partitioned into the two intervals [0, 2] and (2, infinity).
+    Chebyshev polynomial expansions are employed in each interval.
+
+    This function is a wrapper for the Cephes [1]_ routine `k1e`.
 
     References
     ----------
-    .. [dlmf] NIST, Digital Library of Mathematical Functions,
-        https://dlmf.nist.gov/10#PT5
+    .. [1] Cephes Mathematical Functions Library,
+           http://www.netlib.org/cephes/
+
+    Examples
+    --------
+    In the following example `k1` returns 0 whereas `k1e` still returns a
+    useful floating point number.
+
+    >>> from scipy.special import k1, k1e
+    >>> k1(1000.), k1e(1000.)
+    (0., 0.03964813081296021)
+
+    Calculate the function at several points by providing a NumPy array or
+    list for `x`:
+
+    >>> import numpy as np
+    >>> k1e(np.array([0.5, 2., 3.]))
+    array([2.73100971, 1.03347685, 0.80656348])
+
+    Plot the function from 0 to 10.
 
+    >>> import matplotlib.pyplot as plt
+    >>> fig, ax = plt.subplots()
+    >>> x = np.linspace(0., 10., 1000)
+    >>> y = k1e(x)
+    >>> ax.plot(x, y)
+    >>> plt.show()
     """)
 
 add_newdoc("kelvin",
@@ -9593,317 +8573,6 @@ def add_newdoc(name, doc):
 
     """)
 
-add_newdoc("mathieu_a",
-    """
-    mathieu_a(m, q, out=None)
-
-    Characteristic value of even Mathieu functions
-
-    Parameters
-    ----------
-    m : array_like
-        Order of the function
-    q : array_like
-        Parameter of the function
-    out : ndarray, optional
-        Optional output array for the function results
-
-    Returns
-    -------
-    scalar or ndarray
-        Characteristic value for the even solution, ``ce_m(z, q)``, of
-        Mathieu's equation.
-
-    See Also
-    --------
-    mathieu_b, mathieu_cem, mathieu_sem
-
-    """)
-
-add_newdoc("mathieu_b",
-    """
-    mathieu_b(m, q, out=None)
-
-    Characteristic value of odd Mathieu functions
-
-    Parameters
-    ----------
-    m : array_like
-        Order of the function
-    q : array_like
-        Parameter of the function
-    out : ndarray, optional
-        Optional output array for the function results
-
-    Returns
-    -------
-    scalar or ndarray
-        Characteristic value for the odd solution, ``se_m(z, q)``, of Mathieu's
-        equation.
-
-    See Also
-    --------
-    mathieu_a, mathieu_cem, mathieu_sem
-
-    """)
-
-add_newdoc("mathieu_cem",
-    """
-    mathieu_cem(m, q, x, out=None)
-
-    Even Mathieu function and its derivative
-
-    Returns the even Mathieu function, ``ce_m(x, q)``, of order `m` and
-    parameter `q` evaluated at `x` (given in degrees).  Also returns the
-    derivative with respect to `x` of ce_m(x, q)
-
-    Parameters
-    ----------
-    m : array_like
-        Order of the function
-    q : array_like
-        Parameter of the function
-    x : array_like
-        Argument of the function, *given in degrees, not radians*
-    out : tuple of ndarray, optional
-        Optional output arrays for the function results
-
-    Returns
-    -------
-    y : scalar or ndarray
-        Value of the function
-    yp : scalar or ndarray
-        Value of the derivative vs x
-
-    See Also
-    --------
-    mathieu_a, mathieu_b, mathieu_sem
-
-    """)
-
-add_newdoc("mathieu_modcem1",
-    """
-    mathieu_modcem1(m, q, x, out=None)
-
-    Even modified Mathieu function of the first kind and its derivative
-
-    Evaluates the even modified Mathieu function of the first kind,
-    ``Mc1m(x, q)``, and its derivative at `x` for order `m` and parameter
-    `q`.
-
-    Parameters
-    ----------
-    m : array_like
-        Order of the function
-    q : array_like
-        Parameter of the function
-    x : array_like
-        Argument of the function, *given in degrees, not radians*
-    out : tuple of ndarray, optional
-        Optional output arrays for the function results
-
-    Returns
-    -------
-    y : scalar or ndarray
-        Value of the function
-    yp : scalar or ndarray
-        Value of the derivative vs x
-
-    See Also
-    --------
-    mathieu_modsem1
-
-    """)
-
-add_newdoc("mathieu_modcem2",
-    """
-    mathieu_modcem2(m, q, x, out=None)
-
-    Even modified Mathieu function of the second kind and its derivative
-
-    Evaluates the even modified Mathieu function of the second kind,
-    Mc2m(x, q), and its derivative at `x` (given in degrees) for order `m`
-    and parameter `q`.
-
-    Parameters
-    ----------
-    m : array_like
-        Order of the function
-    q : array_like
-        Parameter of the function
-    x : array_like
-        Argument of the function, *given in degrees, not radians*
-    out : tuple of ndarray, optional
-        Optional output arrays for the function results
-
-    Returns
-    -------
-    y : scalar or ndarray
-        Value of the function
-    yp : scalar or ndarray
-        Value of the derivative vs x
-
-    See Also
-    --------
-    mathieu_modsem2
-
-    """)
-
-add_newdoc("mathieu_modsem1",
-    """
-    mathieu_modsem1(m, q, x, out=None)
-
-    Odd modified Mathieu function of the first kind and its derivative
-
-    Evaluates the odd modified Mathieu function of the first kind,
-    Ms1m(x, q), and its derivative at `x` (given in degrees) for order `m`
-    and parameter `q`.
-
-    Parameters
-    ----------
-    m : array_like
-        Order of the function
-    q : array_like
-        Parameter of the function
-    x : array_like
-        Argument of the function, *given in degrees, not radians*
-    out : tuple of ndarray, optional
-        Optional output arrays for the function results
-
-    Returns
-    -------
-    y : scalar or ndarray
-        Value of the function
-    yp : scalar or ndarray
-        Value of the derivative vs x
-
-    See Also
-    --------
-    mathieu_modcem1
-
-    """)
-
-add_newdoc("mathieu_modsem2",
-    """
-    mathieu_modsem2(m, q, x, out=None)
-
-    Odd modified Mathieu function of the second kind and its derivative
-
-    Evaluates the odd modified Mathieu function of the second kind,
-    Ms2m(x, q), and its derivative at `x` (given in degrees) for order `m`
-    and parameter q.
-
-    Parameters
-    ----------
-    m : array_like
-        Order of the function
-    q : array_like
-        Parameter of the function
-    x : array_like
-        Argument of the function, *given in degrees, not radians*
-    out : tuple of ndarray, optional
-        Optional output arrays for the function results
-
-    Returns
-    -------
-    y : scalar or ndarray
-        Value of the function
-    yp : scalar or ndarray
-        Value of the derivative vs x
-
-    See Also
-    --------
-    mathieu_modcem2
-
-    """)
-
-add_newdoc(
-    "mathieu_sem",
-    """
-    mathieu_sem(m, q, x, out=None)
-
-    Odd Mathieu function and its derivative
-
-    Returns the odd Mathieu function, se_m(x, q), of order `m` and
-    parameter `q` evaluated at `x` (given in degrees).  Also returns the
-    derivative with respect to `x` of se_m(x, q).
-
-    Parameters
-    ----------
-    m : array_like
-        Order of the function
-    q : array_like
-        Parameter of the function
-    x : array_like
-        Argument of the function, *given in degrees, not radians*.
-    out : tuple of ndarray, optional
-        Optional output arrays for the function results
-
-    Returns
-    -------
-    y : scalar or ndarray
-        Value of the function
-    yp : scalar or ndarray
-        Value of the derivative vs x
-
-    See Also
-    --------
-    mathieu_a, mathieu_b, mathieu_cem
-
-    """)
-
-add_newdoc("modfresnelm",
-    """
-    modfresnelm(x, out=None)
-
-    Modified Fresnel negative integrals
-
-    Parameters
-    ----------
-    x : array_like
-        Function argument
-    out : tuple of ndarray, optional
-        Optional output arrays for the function results
-
-    Returns
-    -------
-    fm : scalar or ndarray
-        Integral ``F_-(x)``: ``integral(exp(-1j*t*t), t=x..inf)``
-    km : scalar or ndarray
-        Integral ``K_-(x)``: ``1/sqrt(pi)*exp(1j*(x*x+pi/4))*fp``
-
-    See Also
-    --------
-    modfresnelp
-
-    """)
-
-add_newdoc("modfresnelp",
-    """
-    modfresnelp(x, out=None)
-
-    Modified Fresnel positive integrals
-
-    Parameters
-    ----------
-    x : array_like
-        Function argument
-    out : tuple of ndarray, optional
-        Optional output arrays for the function results
-
-    Returns
-    -------
-    fp : scalar or ndarray
-        Integral ``F_+(x)``: ``integral(exp(1j*t*t), t=x..inf)``
-    kp : scalar or ndarray
-        Integral ``K_+(x)``: ``1/sqrt(pi)*exp(-1j*(x*x+pi/4))*fp``
-
-    See Also
-    --------
-    modfresnelm
-
-    """)
-
 add_newdoc("modstruve",
     r"""
     modstruve(v, x, out=None)
@@ -14204,14 +12873,6 @@ def add_newdoc(name, doc):
             2.27727687e-05-1.17951906e-01j]])
     """)
 
-add_newdoc("_zeta",
-    """
-    _zeta(x, q)
-
-    Internal function, Hurwitz zeta.
-
-    """)
-
 add_newdoc("zetac",
     """
     zetac(x, out=None)
@@ -14409,16 +13070,6 @@ def add_newdoc(name, doc):
       Journal of Algorithms, Volume 25, Issue 2, November 1997, pages 221-236.
     """)
 
-add_newdoc("_sinpi",
-    """
-    Internal function, do not use.
-    """)
-
-add_newdoc("_cospi",
-    """
-    Internal function, do not use.
-    """)
-
 add_newdoc("owens_t",
     """
     owens_t(h, a, out=None)
diff --git a/scipy/special/_generate_pyx.py b/scipy/special/_generate_pyx.py
--- a/scipy/special/_generate_pyx.py
+++ b/scipy/special/_generate_pyx.py
@@ -78,6 +78,15 @@
 
 import numpy
 
+special_ufuncs = [
+    '_cospi', '_sinpi', 'bei', 'beip', 'ber', 'berp', 'exp1', 'expi',
+    'gammaln', 'it2i0k0', 'it2j0y0', 'it2struve0', 'itairy',
+    'iti0k0', 'itj0y0', 'itmodstruve0', 'itstruve0', 'kei', 'keip',
+    'kelvin', 'ker', 'kerp', 'mathieu_a', 'mathieu_b',
+    'mathieu_cem', 'mathieu_modcem1', 'mathieu_modcem2', 'mathieu_modsem1',
+    'mathieu_modsem2', 'mathieu_sem', 'modfresnelm', 'modfresnelp', '_zeta'
+]
+
 # -----------------------------------------------------------------------------
 # Extra code
 # -----------------------------------------------------------------------------
@@ -95,11 +104,13 @@
 include "_ufuncs_extra_code.pxi"
 """
 
-UFUNCS_EXTRA_CODE_BOTTOM = """\
+UFUNCS_EXTRA_CODE_BOTTOM = f"""\
 #
 # Aliases
 #
 jn = jv
+
+from ._special_ufuncs import ({', '.join(special_ufuncs)})
 """
 
 CYTHON_SPECIAL_PXD = """\
@@ -367,9 +378,8 @@ def generate_loop(func_inputs, func_outputs, func_retval,
             func_retval != "v" and len(func_outputs)+1 == len(ufunc_outputs)):
         raise ValueError("Function retval and ufunc outputs don't match")
 
-    name = "loop_{}_{}_{}_As_{}_{}".format(
-        func_retval, func_inputs, func_outputs, ufunc_inputs, ufunc_outputs
-        )
+    name = (f"loop_{func_retval}_{func_inputs}_{func_outputs}"
+            f"_As_{ufunc_inputs}_{ufunc_outputs}")
     body = (f"cdef void {name}(char **args, np.npy_intp *dims, np.npy_intp *steps, "
             f"void *data) noexcept nogil:\n")
     body += "    cdef np.npy_intp i, n = dims[0]\n"
@@ -1240,8 +1250,8 @@ def get_declaration(ufunc, c_name, c_proto, cy_proto, header,
         # check function signature at compile time
         proto_name = '_proto_%s_t' % var_name
         defs.append("ctypedef %s" % (cy_proto.replace('(*)', proto_name)))
-        defs.append("cdef {} *{}_var = &{}".format(
-            proto_name, proto_name, ufunc.cython_func_name(c_name, specialized=True)))
+        defs.append(f"cdef {proto_name} *{proto_name}_var = "
+                    f"&{ufunc.cython_func_name(c_name, specialized=True)}")
     else:
         # redeclare the function, so that the assumed
         # signature is checked at compile time
@@ -1295,10 +1305,10 @@ def generate_ufuncs(fn_prefix, cxx_fn_prefix, ufuncs):
                 cxx_defs.extend(item_defs)
                 cxx_defs_h.extend(item_defs_h)
 
-                cxx_defs.append("cdef void *_export_{} = <void*>{}".format(
-                    var_name,
-                    ufunc.cython_func_name(c_name, specialized=True, override=False)
-                ))
+                func_name = ufunc.cython_func_name(
+                    c_name, specialized=True, override=False
+                )
+                cxx_defs.append(f"cdef void *_export_{var_name} = <void*>{func_name}")
                 cxx_pxd_defs.append(f"cdef void *_export_{var_name}")
 
                 # let cython grab the function pointer from the c++ shared library
@@ -1325,7 +1335,11 @@ def generate_ufuncs(fn_prefix, cxx_fn_prefix, ufuncs):
             f"'{ufunc.name}'"
             for ufunc in ufuncs if not ufunc.name.startswith('_')
         ]
-        + ["'geterr'", "'seterr'", "'errstate'", "'jn'"]
+        + ["'geterr'", "'seterr'", "'errstate'", "'jn'"] +
+        [
+            f"'{name}'"
+            for name in special_ufuncs if not name.startswith('_')
+        ]
     )
     module_all = '__all__ = [{}]'.format(', '.join(all_ufuncs))
 
@@ -1428,12 +1442,12 @@ def generate_fused_funcs(modname, ufunc_fn_prefix, fused_funcs):
         f.write("\n\n".join(bench_aux))
 
 
-def generate_ufuncs_type_stubs(module_name: str, ufuncs: list[Ufunc]):
+def generate_ufuncs_type_stubs(module_name: str, ufunc_names: list[str]):
     stubs, module_all = [], []
-    for ufunc in ufuncs:
-        stubs.append(f'{ufunc.name}: np.ufunc')
-        if not ufunc.name.startswith('_'):
-            module_all.append(f"'{ufunc.name}'")
+    for ufunc_name in ufunc_names:
+        stubs.append(f'{ufunc_name}: np.ufunc')
+        if not ufunc_name.startswith('_'):
+            module_all.append(f"'{ufunc_name}'")
     # jn is an alias for jv.
     module_all.append("'jn'")
     stubs.append('jn: np.ufunc')
@@ -1508,17 +1522,19 @@ def main(outdir):
         print("scipy/special/_generate_pyx.py: all files up-to-date")
         return
 
-    ufuncs, fused_funcs = [], []
+    ufuncs, ufuncs_type_stubs, fused_funcs = [], [], []
     with open('functions.json') as data:
         functions = json.load(data)
     for f, sig in functions.items():
-        ufuncs.append(Ufunc(f, sig))
+        if (f not in special_ufuncs):
+            ufuncs.append(Ufunc(f, sig))
+        ufuncs_type_stubs.append(f)
         fused_funcs.append(FusedFunc(f, sig))
     generate_ufuncs(os.path.join(outdir, "_ufuncs"),
                     os.path.join(outdir, "_ufuncs_cxx"),
                     ufuncs)
     generate_ufuncs_type_stubs(os.path.join(outdir, "_ufuncs"),
-                               ufuncs)
+                               ufuncs_type_stubs)
     generate_fused_funcs(os.path.join(outdir, "cython_special"),
                          os.path.join(outdir, "_ufuncs"),
                          fused_funcs)
diff --git a/tools/refguide_check.py b/tools/refguide_check.py
--- a/tools/refguide_check.py
+++ b/tools/refguide_check.py
@@ -117,6 +117,8 @@
     'scipy.linalg.LinAlgError',
     'scipy.optimize.show_options',
     'io.rst',   # XXX: need to figure out how to deal w/ mat files
+    'scipy.special.errstate',
+    'scipy.special.seterr',
 ])
 
 # these names are not required to be present in ALL despite being in


Test patch: