about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/numpy/polynomial
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/numpy/polynomial
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/numpy/polynomial')
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/__init__.py185
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/__init__.pyi22
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/_polybase.py1206
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/_polybase.pyi71
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/chebyshev.py2082
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/chebyshev.pyi51
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/hermite.py1703
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/hermite.pyi46
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/hermite_e.py1695
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/hermite_e.pyi46
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/laguerre.py1651
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/laguerre.pyi46
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/legendre.py1664
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/legendre.pyi46
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/polynomial.py1542
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/polynomial.pyi41
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/polyutils.py789
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/polyutils.pyi11
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/setup.py10
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_chebyshev.py619
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_classes.py600
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_hermite.py555
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_hermite_e.py556
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_laguerre.py537
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_legendre.py568
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_polynomial.py611
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_polyutils.py121
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_printing.py530
-rw-r--r--.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_symbol.py216
30 files changed, 17820 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/__init__.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/__init__.py
new file mode 100644
index 00000000..c4e7baf2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/__init__.py
@@ -0,0 +1,185 @@
+"""
+A sub-package for efficiently dealing with polynomials.
+
+Within the documentation for this sub-package, a "finite power series,"
+i.e., a polynomial (also referred to simply as a "series") is represented
+by a 1-D numpy array of the polynomial's coefficients, ordered from lowest
+order term to highest.  For example, array([1,2,3]) represents
+``P_0 + 2*P_1 + 3*P_2``, where P_n is the n-th order basis polynomial
+applicable to the specific module in question, e.g., `polynomial` (which
+"wraps" the "standard" basis) or `chebyshev`.  For optimal performance,
+all operations on polynomials, including evaluation at an argument, are
+implemented as operations on the coefficients.  Additional (module-specific)
+information can be found in the docstring for the module of interest.
+
+This package provides *convenience classes* for each of six different kinds
+of polynomials:
+
+         ========================    ================
+         **Name**                    **Provides**
+         ========================    ================
+         `~polynomial.Polynomial`    Power series
+         `~chebyshev.Chebyshev`      Chebyshev series
+         `~legendre.Legendre`        Legendre series
+         `~laguerre.Laguerre`        Laguerre series
+         `~hermite.Hermite`          Hermite series
+         `~hermite_e.HermiteE`       HermiteE series
+         ========================    ================
+
+These *convenience classes* provide a consistent interface for creating,
+manipulating, and fitting data with polynomials of different bases.
+The convenience classes are the preferred interface for the `~numpy.polynomial`
+package, and are available from the ``numpy.polynomial`` namespace.
+This eliminates the need to navigate to the corresponding submodules, e.g.
+``np.polynomial.Polynomial`` or ``np.polynomial.Chebyshev`` instead of
+``np.polynomial.polynomial.Polynomial`` or
+``np.polynomial.chebyshev.Chebyshev``, respectively.
+The classes provide a more consistent and concise interface than the
+type-specific functions defined in the submodules for each type of polynomial.
+For example, to fit a Chebyshev polynomial with degree ``1`` to data given
+by arrays ``xdata`` and ``ydata``, the
+`~chebyshev.Chebyshev.fit` class method::
+
+    >>> from numpy.polynomial import Chebyshev
+    >>> c = Chebyshev.fit(xdata, ydata, deg=1)
+
+is preferred over the `chebyshev.chebfit` function from the
+``np.polynomial.chebyshev`` module::
+
+    >>> from numpy.polynomial.chebyshev import chebfit
+    >>> c = chebfit(xdata, ydata, deg=1)
+
+See :doc:`routines.polynomials.classes` for more details.
+
+Convenience Classes
+===================
+
+The following lists the various constants and methods common to all of
+the classes representing the various kinds of polynomials. In the following,
+the term ``Poly`` represents any one of the convenience classes (e.g.
+`~polynomial.Polynomial`, `~chebyshev.Chebyshev`, `~hermite.Hermite`, etc.)
+while the lowercase ``p`` represents an **instance** of a polynomial class.
+
+Constants
+---------
+
+- ``Poly.domain``     -- Default domain
+- ``Poly.window``     -- Default window
+- ``Poly.basis_name`` -- String used to represent the basis
+- ``Poly.maxpower``   -- Maximum value ``n`` such that ``p**n`` is allowed
+- ``Poly.nickname``   -- String used in printing
+
+Creation
+--------
+
+Methods for creating polynomial instances.
+
+- ``Poly.basis(degree)``    -- Basis polynomial of given degree
+- ``Poly.identity()``       -- ``p`` where ``p(x) = x`` for all ``x``
+- ``Poly.fit(x, y, deg)``   -- ``p`` of degree ``deg`` with coefficients
+  determined by the least-squares fit to the data ``x``, ``y``
+- ``Poly.fromroots(roots)`` -- ``p`` with specified roots
+- ``p.copy()``              -- Create a copy of ``p``
+
+Conversion
+----------
+
+Methods for converting a polynomial instance of one kind to another.
+
+- ``p.cast(Poly)``    -- Convert ``p`` to instance of kind ``Poly``
+- ``p.convert(Poly)`` -- Convert ``p`` to instance of kind ``Poly`` or map
+  between ``domain`` and ``window``
+
+Calculus
+--------
+- ``p.deriv()`` -- Take the derivative of ``p``
+- ``p.integ()`` -- Integrate ``p``
+
+Validation
+----------
+- ``Poly.has_samecoef(p1, p2)``   -- Check if coefficients match
+- ``Poly.has_samedomain(p1, p2)`` -- Check if domains match
+- ``Poly.has_sametype(p1, p2)``   -- Check if types match
+- ``Poly.has_samewindow(p1, p2)`` -- Check if windows match
+
+Misc
+----
+- ``p.linspace()`` -- Return ``x, p(x)`` at equally-spaced points in ``domain``
+- ``p.mapparms()`` -- Return the parameters for the linear mapping between
+  ``domain`` and ``window``.
+- ``p.roots()``    -- Return the roots of `p`.
+- ``p.trim()``     -- Remove trailing coefficients.
+- ``p.cutdeg(degree)`` -- Truncate p to given degree
+- ``p.truncate(size)`` -- Truncate p to given size
+
+"""
+from .polynomial import Polynomial
+from .chebyshev import Chebyshev
+from .legendre import Legendre
+from .hermite import Hermite
+from .hermite_e import HermiteE
+from .laguerre import Laguerre
+
+__all__ = [
+    "set_default_printstyle",
+    "polynomial", "Polynomial",
+    "chebyshev", "Chebyshev",
+    "legendre", "Legendre",
+    "hermite", "Hermite",
+    "hermite_e", "HermiteE",
+    "laguerre", "Laguerre",
+]
+
+
+def set_default_printstyle(style):
+    """
+    Set the default format for the string representation of polynomials.
+
+    Values for ``style`` must be valid inputs to ``__format__``, i.e. 'ascii'
+    or 'unicode'.
+
+    Parameters
+    ----------
+    style : str
+        Format string for default printing style. Must be either 'ascii' or
+        'unicode'.
+
+    Notes
+    -----
+    The default format depends on the platform: 'unicode' is used on
+    Unix-based systems and 'ascii' on Windows. This determination is based on
+    default font support for the unicode superscript and subscript ranges.
+
+    Examples
+    --------
+    >>> p = np.polynomial.Polynomial([1, 2, 3])
+    >>> c = np.polynomial.Chebyshev([1, 2, 3])
+    >>> np.polynomial.set_default_printstyle('unicode')
+    >>> print(p)
+    1.0 + 2.0·x + 3.0·x²
+    >>> print(c)
+    1.0 + 2.0·T₁(x) + 3.0·T₂(x)
+    >>> np.polynomial.set_default_printstyle('ascii')
+    >>> print(p)
+    1.0 + 2.0 x + 3.0 x**2
+    >>> print(c)
+    1.0 + 2.0 T_1(x) + 3.0 T_2(x)
+    >>> # Formatting supersedes all class/package-level defaults
+    >>> print(f"{p:unicode}")
+    1.0 + 2.0·x + 3.0·x²
+    """
+    if style not in ('unicode', 'ascii'):
+        raise ValueError(
+            f"Unsupported format string '{style}'. Valid options are 'ascii' "
+            f"and 'unicode'"
+        )
+    _use_unicode = True
+    if style == 'ascii':
+        _use_unicode = False
+    from ._polybase import ABCPolyBase
+    ABCPolyBase._use_unicode = _use_unicode
+
+
+from numpy._pytesttester import PytestTester
+test = PytestTester(__name__)
+del PytestTester
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/__init__.pyi b/.venv/lib/python3.12/site-packages/numpy/polynomial/__init__.pyi
new file mode 100644
index 00000000..c9d1c27a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/__init__.pyi
@@ -0,0 +1,22 @@
+from numpy._pytesttester import PytestTester
+
+from numpy.polynomial import (
+    chebyshev as chebyshev,
+    hermite as hermite,
+    hermite_e as hermite_e,
+    laguerre as laguerre,
+    legendre as legendre,
+    polynomial as polynomial,
+)
+from numpy.polynomial.chebyshev import Chebyshev as Chebyshev
+from numpy.polynomial.hermite import Hermite as Hermite
+from numpy.polynomial.hermite_e import HermiteE as HermiteE
+from numpy.polynomial.laguerre import Laguerre as Laguerre
+from numpy.polynomial.legendre import Legendre as Legendre
+from numpy.polynomial.polynomial import Polynomial as Polynomial
+
+__all__: list[str]
+__path__: list[str]
+test: PytestTester
+
+def set_default_printstyle(style): ...
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/_polybase.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/_polybase.py
new file mode 100644
index 00000000..9730574c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/_polybase.py
@@ -0,0 +1,1206 @@
+"""
+Abstract base class for the various polynomial Classes.
+
+The ABCPolyBase class provides the methods needed to implement the common API
+for the various polynomial classes. It operates as a mixin, but uses the
+abc module from the stdlib, hence it is only available for Python >= 2.6.
+
+"""
+import os
+import abc
+import numbers
+
+import numpy as np
+from . import polyutils as pu
+
+__all__ = ['ABCPolyBase']
+
+class ABCPolyBase(abc.ABC):
+    """An abstract base class for immutable series classes.
+
+    ABCPolyBase provides the standard Python numerical methods
+    '+', '-', '*', '//', '%', 'divmod', '**', and '()' along with the
+    methods listed below.
+
+    .. versionadded:: 1.9.0
+
+    Parameters
+    ----------
+    coef : array_like
+        Series coefficients in order of increasing degree, i.e.,
+        ``(1, 2, 3)`` gives ``1*P_0(x) + 2*P_1(x) + 3*P_2(x)``, where
+        ``P_i`` is the basis polynomials of degree ``i``.
+    domain : (2,) array_like, optional
+        Domain to use. The interval ``[domain[0], domain[1]]`` is mapped
+        to the interval ``[window[0], window[1]]`` by shifting and scaling.
+        The default value is the derived class domain.
+    window : (2,) array_like, optional
+        Window, see domain for its use. The default value is the
+        derived class window.
+    symbol : str, optional
+        Symbol used to represent the independent variable in string 
+        representations of the polynomial expression, e.g. for printing.
+        The symbol must be a valid Python identifier. Default value is 'x'.
+
+        .. versionadded:: 1.24
+
+    Attributes
+    ----------
+    coef : (N,) ndarray
+        Series coefficients in order of increasing degree.
+    domain : (2,) ndarray
+        Domain that is mapped to window.
+    window : (2,) ndarray
+        Window that domain is mapped to.
+    symbol : str
+        Symbol representing the independent variable.
+
+    Class Attributes
+    ----------------
+    maxpower : int
+        Maximum power allowed, i.e., the largest number ``n`` such that
+        ``p(x)**n`` is allowed. This is to limit runaway polynomial size.
+    domain : (2,) ndarray
+        Default domain of the class.
+    window : (2,) ndarray
+        Default window of the class.
+
+    """
+
+    # Not hashable
+    __hash__ = None
+
+    # Opt out of numpy ufuncs and Python ops with ndarray subclasses.
+    __array_ufunc__ = None
+
+    # Limit runaway size. T_n^m has degree n*m
+    maxpower = 100
+
+    # Unicode character mappings for improved __str__
+    _superscript_mapping = str.maketrans({
+        "0": "⁰",
+        "1": "¹",
+        "2": "²",
+        "3": "³",
+        "4": "⁴",
+        "5": "⁵",
+        "6": "⁶",
+        "7": "⁷",
+        "8": "⁸",
+        "9": "⁹"
+    })
+    _subscript_mapping = str.maketrans({
+        "0": "₀",
+        "1": "₁",
+        "2": "₂",
+        "3": "₃",
+        "4": "₄",
+        "5": "₅",
+        "6": "₆",
+        "7": "₇",
+        "8": "₈",
+        "9": "₉"
+    })
+    # Some fonts don't support full unicode character ranges necessary for
+    # the full set of superscripts and subscripts, including common/default
+    # fonts in Windows shells/terminals. Therefore, default to ascii-only
+    # printing on windows.
+    _use_unicode = not os.name == 'nt'
+
+    @property
+    def symbol(self):
+        return self._symbol
+
+    @property
+    @abc.abstractmethod
+    def domain(self):
+        pass
+
+    @property
+    @abc.abstractmethod
+    def window(self):
+        pass
+
+    @property
+    @abc.abstractmethod
+    def basis_name(self):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def _add(c1, c2):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def _sub(c1, c2):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def _mul(c1, c2):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def _div(c1, c2):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def _pow(c, pow, maxpower=None):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def _val(x, c):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def _int(c, m, k, lbnd, scl):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def _der(c, m, scl):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def _fit(x, y, deg, rcond, full):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def _line(off, scl):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def _roots(c):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def _fromroots(r):
+        pass
+
+    def has_samecoef(self, other):
+        """Check if coefficients match.
+
+        .. versionadded:: 1.6.0
+
+        Parameters
+        ----------
+        other : class instance
+            The other class must have the ``coef`` attribute.
+
+        Returns
+        -------
+        bool : boolean
+            True if the coefficients are the same, False otherwise.
+
+        """
+        if len(self.coef) != len(other.coef):
+            return False
+        elif not np.all(self.coef == other.coef):
+            return False
+        else:
+            return True
+
+    def has_samedomain(self, other):
+        """Check if domains match.
+
+        .. versionadded:: 1.6.0
+
+        Parameters
+        ----------
+        other : class instance
+            The other class must have the ``domain`` attribute.
+
+        Returns
+        -------
+        bool : boolean
+            True if the domains are the same, False otherwise.
+
+        """
+        return np.all(self.domain == other.domain)
+
+    def has_samewindow(self, other):
+        """Check if windows match.
+
+        .. versionadded:: 1.6.0
+
+        Parameters
+        ----------
+        other : class instance
+            The other class must have the ``window`` attribute.
+
+        Returns
+        -------
+        bool : boolean
+            True if the windows are the same, False otherwise.
+
+        """
+        return np.all(self.window == other.window)
+
+    def has_sametype(self, other):
+        """Check if types match.
+
+        .. versionadded:: 1.7.0
+
+        Parameters
+        ----------
+        other : object
+            Class instance.
+
+        Returns
+        -------
+        bool : boolean
+            True if other is same class as self
+
+        """
+        return isinstance(other, self.__class__)
+
+    def _get_coefficients(self, other):
+        """Interpret other as polynomial coefficients.
+
+        The `other` argument is checked to see if it is of the same
+        class as self with identical domain and window. If so,
+        return its coefficients, otherwise return `other`.
+
+        .. versionadded:: 1.9.0
+
+        Parameters
+        ----------
+        other : anything
+            Object to be checked.
+
+        Returns
+        -------
+        coef
+            The coefficients of`other` if it is a compatible instance,
+            of ABCPolyBase, otherwise `other`.
+
+        Raises
+        ------
+        TypeError
+            When `other` is an incompatible instance of ABCPolyBase.
+
+        """
+        if isinstance(other, ABCPolyBase):
+            if not isinstance(other, self.__class__):
+                raise TypeError("Polynomial types differ")
+            elif not np.all(self.domain == other.domain):
+                raise TypeError("Domains differ")
+            elif not np.all(self.window == other.window):
+                raise TypeError("Windows differ")
+            elif self.symbol != other.symbol:
+                raise ValueError("Polynomial symbols differ")
+            return other.coef
+        return other
+
+    def __init__(self, coef, domain=None, window=None, symbol='x'):
+        [coef] = pu.as_series([coef], trim=False)
+        self.coef = coef
+
+        if domain is not None:
+            [domain] = pu.as_series([domain], trim=False)
+            if len(domain) != 2:
+                raise ValueError("Domain has wrong number of elements.")
+            self.domain = domain
+
+        if window is not None:
+            [window] = pu.as_series([window], trim=False)
+            if len(window) != 2:
+                raise ValueError("Window has wrong number of elements.")
+            self.window = window
+
+        # Validation for symbol
+        try:
+            if not symbol.isidentifier():
+                raise ValueError(
+                    "Symbol string must be a valid Python identifier"
+                )
+        # If a user passes in something other than a string, the above
+        # results in an AttributeError. Catch this and raise a more
+        # informative exception
+        except AttributeError:
+            raise TypeError("Symbol must be a non-empty string")
+
+        self._symbol = symbol
+
+    def __repr__(self):
+        coef = repr(self.coef)[6:-1]
+        domain = repr(self.domain)[6:-1]
+        window = repr(self.window)[6:-1]
+        name = self.__class__.__name__
+        return (f"{name}({coef}, domain={domain}, window={window}, "
+                f"symbol='{self.symbol}')")
+
+    def __format__(self, fmt_str):
+        if fmt_str == '':
+            return self.__str__()
+        if fmt_str not in ('ascii', 'unicode'):
+            raise ValueError(
+                f"Unsupported format string '{fmt_str}' passed to "
+                f"{self.__class__}.__format__. Valid options are "
+                f"'ascii' and 'unicode'"
+            )
+        if fmt_str == 'ascii':
+            return self._generate_string(self._str_term_ascii)
+        return self._generate_string(self._str_term_unicode)
+
+    def __str__(self):
+        if self._use_unicode:
+            return self._generate_string(self._str_term_unicode)
+        return self._generate_string(self._str_term_ascii)
+
+    def _generate_string(self, term_method):
+        """
+        Generate the full string representation of the polynomial, using
+        ``term_method`` to generate each polynomial term.
+        """
+        # Get configuration for line breaks
+        linewidth = np.get_printoptions().get('linewidth', 75)
+        if linewidth < 1:
+            linewidth = 1
+        out = pu.format_float(self.coef[0])
+        for i, coef in enumerate(self.coef[1:]):
+            out += " "
+            power = str(i + 1)
+            # Polynomial coefficient
+            # The coefficient array can be an object array with elements that
+            # will raise a TypeError with >= 0 (e.g. strings or Python
+            # complex). In this case, represent the coefficient as-is.
+            try:
+                if coef >= 0:
+                    next_term = f"+ " + pu.format_float(coef, parens=True)
+                else:
+                    next_term = f"- " + pu.format_float(-coef, parens=True)
+            except TypeError:
+                next_term = f"+ {coef}"
+            # Polynomial term
+            next_term += term_method(power, self.symbol)
+            # Length of the current line with next term added
+            line_len = len(out.split('\n')[-1]) + len(next_term)
+            # If not the last term in the polynomial, it will be two
+            # characters longer due to the +/- with the next term
+            if i < len(self.coef[1:]) - 1:
+                line_len += 2
+            # Handle linebreaking
+            if line_len >= linewidth:
+                next_term = next_term.replace(" ", "\n", 1)
+            out += next_term
+        return out
+
+    @classmethod
+    def _str_term_unicode(cls, i, arg_str):
+        """
+        String representation of single polynomial term using unicode
+        characters for superscripts and subscripts.
+        """
+        if cls.basis_name is None:
+            raise NotImplementedError(
+                "Subclasses must define either a basis_name, or override "
+                "_str_term_unicode(cls, i, arg_str)"
+            )
+        return (f"·{cls.basis_name}{i.translate(cls._subscript_mapping)}"
+                f"({arg_str})")
+
+    @classmethod
+    def _str_term_ascii(cls, i, arg_str):
+        """
+        String representation of a single polynomial term using ** and _ to
+        represent superscripts and subscripts, respectively.
+        """
+        if cls.basis_name is None:
+            raise NotImplementedError(
+                "Subclasses must define either a basis_name, or override "
+                "_str_term_ascii(cls, i, arg_str)"
+            )
+        return f" {cls.basis_name}_{i}({arg_str})"
+
+    @classmethod
+    def _repr_latex_term(cls, i, arg_str, needs_parens):
+        if cls.basis_name is None:
+            raise NotImplementedError(
+                "Subclasses must define either a basis name, or override "
+                "_repr_latex_term(i, arg_str, needs_parens)")
+        # since we always add parens, we don't care if the expression needs them
+        return f"{{{cls.basis_name}}}_{{{i}}}({arg_str})"
+
+    @staticmethod
+    def _repr_latex_scalar(x, parens=False):
+        # TODO: we're stuck with disabling math formatting until we handle
+        # exponents in this function
+        return r'\text{{{}}}'.format(pu.format_float(x, parens=parens))
+
+    def _repr_latex_(self):
+        # get the scaled argument string to the basis functions
+        off, scale = self.mapparms()
+        if off == 0 and scale == 1:
+            term = self.symbol
+            needs_parens = False
+        elif scale == 1:
+            term = f"{self._repr_latex_scalar(off)} + {self.symbol}"
+            needs_parens = True
+        elif off == 0:
+            term = f"{self._repr_latex_scalar(scale)}{self.symbol}"
+            needs_parens = True
+        else:
+            term = (
+                f"{self._repr_latex_scalar(off)} + "
+                f"{self._repr_latex_scalar(scale)}{self.symbol}"
+            )
+            needs_parens = True
+
+        mute = r"\color{{LightGray}}{{{}}}".format
+
+        parts = []
+        for i, c in enumerate(self.coef):
+            # prevent duplication of + and - signs
+            if i == 0:
+                coef_str = f"{self._repr_latex_scalar(c)}"
+            elif not isinstance(c, numbers.Real):
+                coef_str = f" + ({self._repr_latex_scalar(c)})"
+            elif not np.signbit(c):
+                coef_str = f" + {self._repr_latex_scalar(c, parens=True)}"
+            else:
+                coef_str = f" - {self._repr_latex_scalar(-c, parens=True)}"
+
+            # produce the string for the term
+            term_str = self._repr_latex_term(i, term, needs_parens)
+            if term_str == '1':
+                part = coef_str
+            else:
+                part = rf"{coef_str}\,{term_str}"
+
+            if c == 0:
+                part = mute(part)
+
+            parts.append(part)
+
+        if parts:
+            body = ''.join(parts)
+        else:
+            # in case somehow there are no coefficients at all
+            body = '0'
+
+        return rf"${self.symbol} \mapsto {body}$"
+
+
+
+    # Pickle and copy
+
+    def __getstate__(self):
+        ret = self.__dict__.copy()
+        ret['coef'] = self.coef.copy()
+        ret['domain'] = self.domain.copy()
+        ret['window'] = self.window.copy()
+        ret['symbol'] = self.symbol
+        return ret
+
+    def __setstate__(self, dict):
+        self.__dict__ = dict
+
+    # Call
+
+    def __call__(self, arg):
+        off, scl = pu.mapparms(self.domain, self.window)
+        arg = off + scl*arg
+        return self._val(arg, self.coef)
+
+    def __iter__(self):
+        return iter(self.coef)
+
+    def __len__(self):
+        return len(self.coef)
+
+    # Numeric properties.
+
+    def __neg__(self):
+        return self.__class__(
+            -self.coef, self.domain, self.window, self.symbol
+        )
+
+    def __pos__(self):
+        return self
+
+    def __add__(self, other):
+        othercoef = self._get_coefficients(other)
+        try:
+            coef = self._add(self.coef, othercoef)
+        except Exception:
+            return NotImplemented
+        return self.__class__(coef, self.domain, self.window, self.symbol)
+
+    def __sub__(self, other):
+        othercoef = self._get_coefficients(other)
+        try:
+            coef = self._sub(self.coef, othercoef)
+        except Exception:
+            return NotImplemented
+        return self.__class__(coef, self.domain, self.window, self.symbol)
+
+    def __mul__(self, other):
+        othercoef = self._get_coefficients(other)
+        try:
+            coef = self._mul(self.coef, othercoef)
+        except Exception:
+            return NotImplemented
+        return self.__class__(coef, self.domain, self.window, self.symbol)
+
+    def __truediv__(self, other):
+        # there is no true divide if the rhs is not a Number, although it
+        # could return the first n elements of an infinite series.
+        # It is hard to see where n would come from, though.
+        if not isinstance(other, numbers.Number) or isinstance(other, bool):
+            raise TypeError(
+                f"unsupported types for true division: "
+                f"'{type(self)}', '{type(other)}'"
+            )
+        return self.__floordiv__(other)
+
+    def __floordiv__(self, other):
+        res = self.__divmod__(other)
+        if res is NotImplemented:
+            return res
+        return res[0]
+
+    def __mod__(self, other):
+        res = self.__divmod__(other)
+        if res is NotImplemented:
+            return res
+        return res[1]
+
+    def __divmod__(self, other):
+        othercoef = self._get_coefficients(other)
+        try:
+            quo, rem = self._div(self.coef, othercoef)
+        except ZeroDivisionError:
+            raise
+        except Exception:
+            return NotImplemented
+        quo = self.__class__(quo, self.domain, self.window, self.symbol)
+        rem = self.__class__(rem, self.domain, self.window, self.symbol)
+        return quo, rem
+
+    def __pow__(self, other):
+        coef = self._pow(self.coef, other, maxpower=self.maxpower)
+        res = self.__class__(coef, self.domain, self.window, self.symbol)
+        return res
+
+    def __radd__(self, other):
+        try:
+            coef = self._add(other, self.coef)
+        except Exception:
+            return NotImplemented
+        return self.__class__(coef, self.domain, self.window, self.symbol)
+
+    def __rsub__(self, other):
+        try:
+            coef = self._sub(other, self.coef)
+        except Exception:
+            return NotImplemented
+        return self.__class__(coef, self.domain, self.window, self.symbol)
+
+    def __rmul__(self, other):
+        try:
+            coef = self._mul(other, self.coef)
+        except Exception:
+            return NotImplemented
+        return self.__class__(coef, self.domain, self.window, self.symbol)
+
+    def __rdiv__(self, other):
+        # set to __floordiv__ /.
+        return self.__rfloordiv__(other)
+
+    def __rtruediv__(self, other):
+        # An instance of ABCPolyBase is not considered a
+        # Number.
+        return NotImplemented
+
+    def __rfloordiv__(self, other):
+        res = self.__rdivmod__(other)
+        if res is NotImplemented:
+            return res
+        return res[0]
+
+    def __rmod__(self, other):
+        res = self.__rdivmod__(other)
+        if res is NotImplemented:
+            return res
+        return res[1]
+
+    def __rdivmod__(self, other):
+        try:
+            quo, rem = self._div(other, self.coef)
+        except ZeroDivisionError:
+            raise
+        except Exception:
+            return NotImplemented
+        quo = self.__class__(quo, self.domain, self.window, self.symbol)
+        rem = self.__class__(rem, self.domain, self.window, self.symbol)
+        return quo, rem
+
+    def __eq__(self, other):
+        res = (isinstance(other, self.__class__) and
+               np.all(self.domain == other.domain) and
+               np.all(self.window == other.window) and
+               (self.coef.shape == other.coef.shape) and
+               np.all(self.coef == other.coef) and
+               (self.symbol == other.symbol))
+        return res
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    #
+    # Extra methods.
+    #
+
+    def copy(self):
+        """Return a copy.
+
+        Returns
+        -------
+        new_series : series
+            Copy of self.
+
+        """
+        return self.__class__(self.coef, self.domain, self.window, self.symbol)
+
+    def degree(self):
+        """The degree of the series.
+
+        .. versionadded:: 1.5.0
+
+        Returns
+        -------
+        degree : int
+            Degree of the series, one less than the number of coefficients.
+
+        Examples
+        --------
+
+        Create a polynomial object for ``1 + 7*x + 4*x**2``:
+
+        >>> poly = np.polynomial.Polynomial([1, 7, 4])
+        >>> print(poly)
+        1.0 + 7.0·x + 4.0·x²
+        >>> poly.degree()
+        2
+
+        Note that this method does not check for non-zero coefficients.
+        You must trim the polynomial to remove any trailing zeroes:
+
+        >>> poly = np.polynomial.Polynomial([1, 7, 0])
+        >>> print(poly)
+        1.0 + 7.0·x + 0.0·x²
+        >>> poly.degree()
+        2
+        >>> poly.trim().degree()
+        1
+
+        """
+        return len(self) - 1
+
+    def cutdeg(self, deg):
+        """Truncate series to the given degree.
+
+        Reduce the degree of the series to `deg` by discarding the
+        high order terms. If `deg` is greater than the current degree a
+        copy of the current series is returned. This can be useful in least
+        squares where the coefficients of the high degree terms may be very
+        small.
+
+        .. versionadded:: 1.5.0
+
+        Parameters
+        ----------
+        deg : non-negative int
+            The series is reduced to degree `deg` by discarding the high
+            order terms. The value of `deg` must be a non-negative integer.
+
+        Returns
+        -------
+        new_series : series
+            New instance of series with reduced degree.
+
+        """
+        return self.truncate(deg + 1)
+
+    def trim(self, tol=0):
+        """Remove trailing coefficients
+
+        Remove trailing coefficients until a coefficient is reached whose
+        absolute value greater than `tol` or the beginning of the series is
+        reached. If all the coefficients would be removed the series is set
+        to ``[0]``. A new series instance is returned with the new
+        coefficients.  The current instance remains unchanged.
+
+        Parameters
+        ----------
+        tol : non-negative number.
+            All trailing coefficients less than `tol` will be removed.
+
+        Returns
+        -------
+        new_series : series
+            New instance of series with trimmed coefficients.
+
+        """
+        coef = pu.trimcoef(self.coef, tol)
+        return self.__class__(coef, self.domain, self.window, self.symbol)
+
+    def truncate(self, size):
+        """Truncate series to length `size`.
+
+        Reduce the series to length `size` by discarding the high
+        degree terms. The value of `size` must be a positive integer. This
+        can be useful in least squares where the coefficients of the
+        high degree terms may be very small.
+
+        Parameters
+        ----------
+        size : positive int
+            The series is reduced to length `size` by discarding the high
+            degree terms. The value of `size` must be a positive integer.
+
+        Returns
+        -------
+        new_series : series
+            New instance of series with truncated coefficients.
+
+        """
+        isize = int(size)
+        if isize != size or isize < 1:
+            raise ValueError("size must be a positive integer")
+        if isize >= len(self.coef):
+            coef = self.coef
+        else:
+            coef = self.coef[:isize]
+        return self.__class__(coef, self.domain, self.window, self.symbol)
+
+    def convert(self, domain=None, kind=None, window=None):
+        """Convert series to a different kind and/or domain and/or window.
+
+        Parameters
+        ----------
+        domain : array_like, optional
+            The domain of the converted series. If the value is None,
+            the default domain of `kind` is used.
+        kind : class, optional
+            The polynomial series type class to which the current instance
+            should be converted. If kind is None, then the class of the
+            current instance is used.
+        window : array_like, optional
+            The window of the converted series. If the value is None,
+            the default window of `kind` is used.
+
+        Returns
+        -------
+        new_series : series
+            The returned class can be of different type than the current
+            instance and/or have a different domain and/or different
+            window.
+
+        Notes
+        -----
+        Conversion between domains and class types can result in
+        numerically ill defined series.
+
+        """
+        if kind is None:
+            kind = self.__class__
+        if domain is None:
+            domain = kind.domain
+        if window is None:
+            window = kind.window
+        return self(kind.identity(domain, window=window, symbol=self.symbol))
+
+    def mapparms(self):
+        """Return the mapping parameters.
+
+        The returned values define a linear map ``off + scl*x`` that is
+        applied to the input arguments before the series is evaluated. The
+        map depends on the ``domain`` and ``window``; if the current
+        ``domain`` is equal to the ``window`` the resulting map is the
+        identity.  If the coefficients of the series instance are to be
+        used by themselves outside this class, then the linear function
+        must be substituted for the ``x`` in the standard representation of
+        the base polynomials.
+
+        Returns
+        -------
+        off, scl : float or complex
+            The mapping function is defined by ``off + scl*x``.
+
+        Notes
+        -----
+        If the current domain is the interval ``[l1, r1]`` and the window
+        is ``[l2, r2]``, then the linear mapping function ``L`` is
+        defined by the equations::
+
+            L(l1) = l2
+            L(r1) = r2
+
+        """
+        return pu.mapparms(self.domain, self.window)
+
+    def integ(self, m=1, k=[], lbnd=None):
+        """Integrate.
+
+        Return a series instance that is the definite integral of the
+        current series.
+
+        Parameters
+        ----------
+        m : non-negative int
+            The number of integrations to perform.
+        k : array_like
+            Integration constants. The first constant is applied to the
+            first integration, the second to the second, and so on. The
+            list of values must less than or equal to `m` in length and any
+            missing values are set to zero.
+        lbnd : Scalar
+            The lower bound of the definite integral.
+
+        Returns
+        -------
+        new_series : series
+            A new series representing the integral. The domain is the same
+            as the domain of the integrated series.
+
+        """
+        off, scl = self.mapparms()
+        if lbnd is None:
+            lbnd = 0
+        else:
+            lbnd = off + scl*lbnd
+        coef = self._int(self.coef, m, k, lbnd, 1./scl)
+        return self.__class__(coef, self.domain, self.window, self.symbol)
+
+    def deriv(self, m=1):
+        """Differentiate.
+
+        Return a series instance of that is the derivative of the current
+        series.
+
+        Parameters
+        ----------
+        m : non-negative int
+            Find the derivative of order `m`.
+
+        Returns
+        -------
+        new_series : series
+            A new series representing the derivative. The domain is the same
+            as the domain of the differentiated series.
+
+        """
+        off, scl = self.mapparms()
+        coef = self._der(self.coef, m, scl)
+        return self.__class__(coef, self.domain, self.window, self.symbol)
+
+    def roots(self):
+        """Return the roots of the series polynomial.
+
+        Compute the roots for the series. Note that the accuracy of the
+        roots decreases the further outside the `domain` they lie.
+
+        Returns
+        -------
+        roots : ndarray
+            Array containing the roots of the series.
+
+        """
+        roots = self._roots(self.coef)
+        return pu.mapdomain(roots, self.window, self.domain)
+
+    def linspace(self, n=100, domain=None):
+        """Return x, y values at equally spaced points in domain.
+
+        Returns the x, y values at `n` linearly spaced points across the
+        domain.  Here y is the value of the polynomial at the points x. By
+        default the domain is the same as that of the series instance.
+        This method is intended mostly as a plotting aid.
+
+        .. versionadded:: 1.5.0
+
+        Parameters
+        ----------
+        n : int, optional
+            Number of point pairs to return. The default value is 100.
+        domain : {None, array_like}, optional
+            If not None, the specified domain is used instead of that of
+            the calling instance. It should be of the form ``[beg,end]``.
+            The default is None which case the class domain is used.
+
+        Returns
+        -------
+        x, y : ndarray
+            x is equal to linspace(self.domain[0], self.domain[1], n) and
+            y is the series evaluated at element of x.
+
+        """
+        if domain is None:
+            domain = self.domain
+        x = np.linspace(domain[0], domain[1], n)
+        y = self(x)
+        return x, y
+
+    @classmethod
+    def fit(cls, x, y, deg, domain=None, rcond=None, full=False, w=None,
+        window=None, symbol='x'):
+        """Least squares fit to data.
+
+        Return a series instance that is the least squares fit to the data
+        `y` sampled at `x`. The domain of the returned instance can be
+        specified and this will often result in a superior fit with less
+        chance of ill conditioning.
+
+        Parameters
+        ----------
+        x : array_like, shape (M,)
+            x-coordinates of the M sample points ``(x[i], y[i])``.
+        y : array_like, shape (M,)
+            y-coordinates of the M sample points ``(x[i], y[i])``.
+        deg : int or 1-D array_like
+            Degree(s) of the fitting polynomials. If `deg` is a single integer
+            all terms up to and including the `deg`'th term are included in the
+            fit. For NumPy versions >= 1.11.0 a list of integers specifying the
+            degrees of the terms to include may be used instead.
+        domain : {None, [beg, end], []}, optional
+            Domain to use for the returned series. If ``None``,
+            then a minimal domain that covers the points `x` is chosen.  If
+            ``[]`` the class domain is used. The default value was the
+            class domain in NumPy 1.4 and ``None`` in later versions.
+            The ``[]`` option was added in numpy 1.5.0.
+        rcond : float, optional
+            Relative condition number of the fit. Singular values smaller
+            than this relative to the largest singular value will be
+            ignored. The default value is len(x)*eps, where eps is the
+            relative precision of the float type, about 2e-16 in most
+            cases.
+        full : bool, optional
+            Switch determining nature of return value. When it is False
+            (the default) just the coefficients are returned, when True
+            diagnostic information from the singular value decomposition is
+            also returned.
+        w : array_like, shape (M,), optional
+            Weights. If not None, the weight ``w[i]`` applies to the unsquared
+            residual ``y[i] - y_hat[i]`` at ``x[i]``. Ideally the weights are
+            chosen so that the errors of the products ``w[i]*y[i]`` all have
+            the same variance.  When using inverse-variance weighting, use
+            ``w[i] = 1/sigma(y[i])``.  The default value is None.
+
+            .. versionadded:: 1.5.0
+        window : {[beg, end]}, optional
+            Window to use for the returned series. The default
+            value is the default class domain
+
+            .. versionadded:: 1.6.0
+        symbol : str, optional
+            Symbol representing the independent variable. Default is 'x'.
+
+        Returns
+        -------
+        new_series : series
+            A series that represents the least squares fit to the data and
+            has the domain and window specified in the call. If the
+            coefficients for the unscaled and unshifted basis polynomials are
+            of interest, do ``new_series.convert().coef``.
+
+        [resid, rank, sv, rcond] : list
+            These values are only returned if ``full == True``
+
+            - resid -- sum of squared residuals of the least squares fit
+            - rank -- the numerical rank of the scaled Vandermonde matrix
+            - sv -- singular values of the scaled Vandermonde matrix
+            - rcond -- value of `rcond`.
+
+            For more details, see `linalg.lstsq`.
+
+        """
+        if domain is None:
+            domain = pu.getdomain(x)
+        elif type(domain) is list and len(domain) == 0:
+            domain = cls.domain
+
+        if window is None:
+            window = cls.window
+
+        xnew = pu.mapdomain(x, domain, window)
+        res = cls._fit(xnew, y, deg, w=w, rcond=rcond, full=full)
+        if full:
+            [coef, status] = res
+            return (
+                cls(coef, domain=domain, window=window, symbol=symbol), status
+            )
+        else:
+            coef = res
+            return cls(coef, domain=domain, window=window, symbol=symbol)
+
+    @classmethod
+    def fromroots(cls, roots, domain=[], window=None, symbol='x'):
+        """Return series instance that has the specified roots.
+
+        Returns a series representing the product
+        ``(x - r[0])*(x - r[1])*...*(x - r[n-1])``, where ``r`` is a
+        list of roots.
+
+        Parameters
+        ----------
+        roots : array_like
+            List of roots.
+        domain : {[], None, array_like}, optional
+            Domain for the resulting series. If None the domain is the
+            interval from the smallest root to the largest. If [] the
+            domain is the class domain. The default is [].
+        window : {None, array_like}, optional
+            Window for the returned series. If None the class window is
+            used. The default is None.
+        symbol : str, optional
+            Symbol representing the independent variable. Default is 'x'.
+
+        Returns
+        -------
+        new_series : series
+            Series with the specified roots.
+
+        """
+        [roots] = pu.as_series([roots], trim=False)
+        if domain is None:
+            domain = pu.getdomain(roots)
+        elif type(domain) is list and len(domain) == 0:
+            domain = cls.domain
+
+        if window is None:
+            window = cls.window
+
+        deg = len(roots)
+        off, scl = pu.mapparms(domain, window)
+        rnew = off + scl*roots
+        coef = cls._fromroots(rnew) / scl**deg
+        return cls(coef, domain=domain, window=window, symbol=symbol)
+
+    @classmethod
+    def identity(cls, domain=None, window=None, symbol='x'):
+        """Identity function.
+
+        If ``p`` is the returned series, then ``p(x) == x`` for all
+        values of x.
+
+        Parameters
+        ----------
+        domain : {None, array_like}, optional
+            If given, the array must be of the form ``[beg, end]``, where
+            ``beg`` and ``end`` are the endpoints of the domain. If None is
+            given then the class domain is used. The default is None.
+        window : {None, array_like}, optional
+            If given, the resulting array must be if the form
+            ``[beg, end]``, where ``beg`` and ``end`` are the endpoints of
+            the window. If None is given then the class window is used. The
+            default is None.
+        symbol : str, optional
+            Symbol representing the independent variable. Default is 'x'.
+
+        Returns
+        -------
+        new_series : series
+             Series of representing the identity.
+
+        """
+        if domain is None:
+            domain = cls.domain
+        if window is None:
+            window = cls.window
+        off, scl = pu.mapparms(window, domain)
+        coef = cls._line(off, scl)
+        return cls(coef, domain, window, symbol)
+
+    @classmethod
+    def basis(cls, deg, domain=None, window=None, symbol='x'):
+        """Series basis polynomial of degree `deg`.
+
+        Returns the series representing the basis polynomial of degree `deg`.
+
+        .. versionadded:: 1.7.0
+
+        Parameters
+        ----------
+        deg : int
+            Degree of the basis polynomial for the series. Must be >= 0.
+        domain : {None, array_like}, optional
+            If given, the array must be of the form ``[beg, end]``, where
+            ``beg`` and ``end`` are the endpoints of the domain. If None is
+            given then the class domain is used. The default is None.
+        window : {None, array_like}, optional
+            If given, the resulting array must be if the form
+            ``[beg, end]``, where ``beg`` and ``end`` are the endpoints of
+            the window. If None is given then the class window is used. The
+            default is None.
+        symbol : str, optional
+            Symbol representing the independent variable. Default is 'x'.
+
+        Returns
+        -------
+        new_series : series
+            A series with the coefficient of the `deg` term set to one and
+            all others zero.
+
+        """
+        if domain is None:
+            domain = cls.domain
+        if window is None:
+            window = cls.window
+        ideg = int(deg)
+
+        if ideg != deg or ideg < 0:
+            raise ValueError("deg must be non-negative integer")
+        return cls([0]*ideg + [1], domain, window, symbol)
+
+    @classmethod
+    def cast(cls, series, domain=None, window=None):
+        """Convert series to series of this class.
+
+        The `series` is expected to be an instance of some polynomial
+        series of one of the types supported by by the numpy.polynomial
+        module, but could be some other class that supports the convert
+        method.
+
+        .. versionadded:: 1.7.0
+
+        Parameters
+        ----------
+        series : series
+            The series instance to be converted.
+        domain : {None, array_like}, optional
+            If given, the array must be of the form ``[beg, end]``, where
+            ``beg`` and ``end`` are the endpoints of the domain. If None is
+            given then the class domain is used. The default is None.
+        window : {None, array_like}, optional
+            If given, the resulting array must be if the form
+            ``[beg, end]``, where ``beg`` and ``end`` are the endpoints of
+            the window. If None is given then the class window is used. The
+            default is None.
+
+        Returns
+        -------
+        new_series : series
+            A series of the same kind as the calling class and equal to
+            `series` when evaluated.
+
+        See Also
+        --------
+        convert : similar instance method
+
+        """
+        if domain is None:
+            domain = cls.domain
+        if window is None:
+            window = cls.window
+        return series.convert(domain, cls, window)
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/_polybase.pyi b/.venv/lib/python3.12/site-packages/numpy/polynomial/_polybase.pyi
new file mode 100644
index 00000000..25c740db
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/_polybase.pyi
@@ -0,0 +1,71 @@
+import abc
+from typing import Any, ClassVar
+
+__all__: list[str]
+
+class ABCPolyBase(abc.ABC):
+    __hash__: ClassVar[None]  # type: ignore[assignment]
+    __array_ufunc__: ClassVar[None]
+    maxpower: ClassVar[int]
+    coef: Any
+    @property
+    def symbol(self) -> str: ...
+    @property
+    @abc.abstractmethod
+    def domain(self): ...
+    @property
+    @abc.abstractmethod
+    def window(self): ...
+    @property
+    @abc.abstractmethod
+    def basis_name(self): ...
+    def has_samecoef(self, other): ...
+    def has_samedomain(self, other): ...
+    def has_samewindow(self, other): ...
+    def has_sametype(self, other): ...
+    def __init__(self, coef, domain=..., window=..., symbol: str = ...) -> None: ...
+    def __format__(self, fmt_str): ...
+    def __call__(self, arg): ...
+    def __iter__(self): ...
+    def __len__(self): ...
+    def __neg__(self): ...
+    def __pos__(self): ...
+    def __add__(self, other): ...
+    def __sub__(self, other): ...
+    def __mul__(self, other): ...
+    def __truediv__(self, other): ...
+    def __floordiv__(self, other): ...
+    def __mod__(self, other): ...
+    def __divmod__(self, other): ...
+    def __pow__(self, other): ...
+    def __radd__(self, other): ...
+    def __rsub__(self, other): ...
+    def __rmul__(self, other): ...
+    def __rdiv__(self, other): ...
+    def __rtruediv__(self, other): ...
+    def __rfloordiv__(self, other): ...
+    def __rmod__(self, other): ...
+    def __rdivmod__(self, other): ...
+    def __eq__(self, other): ...
+    def __ne__(self, other): ...
+    def copy(self): ...
+    def degree(self): ...
+    def cutdeg(self, deg): ...
+    def trim(self, tol=...): ...
+    def truncate(self, size): ...
+    def convert(self, domain=..., kind=..., window=...): ...
+    def mapparms(self): ...
+    def integ(self, m=..., k = ..., lbnd=...): ...
+    def deriv(self, m=...): ...
+    def roots(self): ...
+    def linspace(self, n=..., domain=...): ...
+    @classmethod
+    def fit(cls, x, y, deg, domain=..., rcond=..., full=..., w=..., window=...): ...
+    @classmethod
+    def fromroots(cls, roots, domain = ..., window=...): ...
+    @classmethod
+    def identity(cls, domain=..., window=...): ...
+    @classmethod
+    def basis(cls, deg, domain=..., window=...): ...
+    @classmethod
+    def cast(cls, series, domain=..., window=...): ...
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/chebyshev.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/chebyshev.py
new file mode 100644
index 00000000..efbe13e0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/chebyshev.py
@@ -0,0 +1,2082 @@
+"""
+====================================================
+Chebyshev Series (:mod:`numpy.polynomial.chebyshev`)
+====================================================
+
+This module provides a number of objects (mostly functions) useful for
+dealing with Chebyshev series, including a `Chebyshev` class that
+encapsulates the usual arithmetic operations.  (General information
+on how this module represents and works with such polynomials is in the
+docstring for its "parent" sub-package, `numpy.polynomial`).
+
+Classes
+-------
+
+.. autosummary::
+   :toctree: generated/
+
+   Chebyshev
+
+
+Constants
+---------
+
+.. autosummary::
+   :toctree: generated/
+
+   chebdomain
+   chebzero
+   chebone
+   chebx
+
+Arithmetic
+----------
+
+.. autosummary::
+   :toctree: generated/
+
+   chebadd
+   chebsub
+   chebmulx
+   chebmul
+   chebdiv
+   chebpow
+   chebval
+   chebval2d
+   chebval3d
+   chebgrid2d
+   chebgrid3d
+
+Calculus
+--------
+
+.. autosummary::
+   :toctree: generated/
+
+   chebder
+   chebint
+
+Misc Functions
+--------------
+
+.. autosummary::
+   :toctree: generated/
+
+   chebfromroots
+   chebroots
+   chebvander
+   chebvander2d
+   chebvander3d
+   chebgauss
+   chebweight
+   chebcompanion
+   chebfit
+   chebpts1
+   chebpts2
+   chebtrim
+   chebline
+   cheb2poly
+   poly2cheb
+   chebinterpolate
+
+See also
+--------
+`numpy.polynomial`
+
+Notes
+-----
+The implementations of multiplication, division, integration, and
+differentiation use the algebraic identities [1]_:
+
+.. math::
+    T_n(x) = \\frac{z^n + z^{-n}}{2} \\\\
+    z\\frac{dx}{dz} = \\frac{z - z^{-1}}{2}.
+
+where
+
+.. math:: x = \\frac{z + z^{-1}}{2}.
+
+These identities allow a Chebyshev series to be expressed as a finite,
+symmetric Laurent series.  In this module, this sort of Laurent series
+is referred to as a "z-series."
+
+References
+----------
+.. [1] A. T. Benjamin, et al., "Combinatorial Trigonometry with Chebyshev
+  Polynomials," *Journal of Statistical Planning and Inference 14*, 2008
+  (https://web.archive.org/web/20080221202153/https://www.math.hmc.edu/~benjamin/papers/CombTrig.pdf, pg. 4)
+
+"""
+import numpy as np
+import numpy.linalg as la
+from numpy.core.multiarray import normalize_axis_index
+
+from . import polyutils as pu
+from ._polybase import ABCPolyBase
+
+__all__ = [
+    'chebzero', 'chebone', 'chebx', 'chebdomain', 'chebline', 'chebadd',
+    'chebsub', 'chebmulx', 'chebmul', 'chebdiv', 'chebpow', 'chebval',
+    'chebder', 'chebint', 'cheb2poly', 'poly2cheb', 'chebfromroots',
+    'chebvander', 'chebfit', 'chebtrim', 'chebroots', 'chebpts1',
+    'chebpts2', 'Chebyshev', 'chebval2d', 'chebval3d', 'chebgrid2d',
+    'chebgrid3d', 'chebvander2d', 'chebvander3d', 'chebcompanion',
+    'chebgauss', 'chebweight', 'chebinterpolate']
+
+chebtrim = pu.trimcoef
+
+#
+# A collection of functions for manipulating z-series. These are private
+# functions and do minimal error checking.
+#
+
+def _cseries_to_zseries(c):
+    """Convert Chebyshev series to z-series.
+
+    Convert a Chebyshev series to the equivalent z-series. The result is
+    never an empty array. The dtype of the return is the same as that of
+    the input. No checks are run on the arguments as this routine is for
+    internal use.
+
+    Parameters
+    ----------
+    c : 1-D ndarray
+        Chebyshev coefficients, ordered from low to high
+
+    Returns
+    -------
+    zs : 1-D ndarray
+        Odd length symmetric z-series, ordered from  low to high.
+
+    """
+    n = c.size
+    zs = np.zeros(2*n-1, dtype=c.dtype)
+    zs[n-1:] = c/2
+    return zs + zs[::-1]
+
+
+def _zseries_to_cseries(zs):
+    """Convert z-series to a Chebyshev series.
+
+    Convert a z series to the equivalent Chebyshev series. The result is
+    never an empty array. The dtype of the return is the same as that of
+    the input. No checks are run on the arguments as this routine is for
+    internal use.
+
+    Parameters
+    ----------
+    zs : 1-D ndarray
+        Odd length symmetric z-series, ordered from  low to high.
+
+    Returns
+    -------
+    c : 1-D ndarray
+        Chebyshev coefficients, ordered from  low to high.
+
+    """
+    n = (zs.size + 1)//2
+    c = zs[n-1:].copy()
+    c[1:n] *= 2
+    return c
+
+
+def _zseries_mul(z1, z2):
+    """Multiply two z-series.
+
+    Multiply two z-series to produce a z-series.
+
+    Parameters
+    ----------
+    z1, z2 : 1-D ndarray
+        The arrays must be 1-D but this is not checked.
+
+    Returns
+    -------
+    product : 1-D ndarray
+        The product z-series.
+
+    Notes
+    -----
+    This is simply convolution. If symmetric/anti-symmetric z-series are
+    denoted by S/A then the following rules apply:
+
+    S*S, A*A -> S
+    S*A, A*S -> A
+
+    """
+    return np.convolve(z1, z2)
+
+
+def _zseries_div(z1, z2):
+    """Divide the first z-series by the second.
+
+    Divide `z1` by `z2` and return the quotient and remainder as z-series.
+    Warning: this implementation only applies when both z1 and z2 have the
+    same symmetry, which is sufficient for present purposes.
+
+    Parameters
+    ----------
+    z1, z2 : 1-D ndarray
+        The arrays must be 1-D and have the same symmetry, but this is not
+        checked.
+
+    Returns
+    -------
+
+    (quotient, remainder) : 1-D ndarrays
+        Quotient and remainder as z-series.
+
+    Notes
+    -----
+    This is not the same as polynomial division on account of the desired form
+    of the remainder. If symmetric/anti-symmetric z-series are denoted by S/A
+    then the following rules apply:
+
+    S/S -> S,S
+    A/A -> S,A
+
+    The restriction to types of the same symmetry could be fixed but seems like
+    unneeded generality. There is no natural form for the remainder in the case
+    where there is no symmetry.
+
+    """
+    z1 = z1.copy()
+    z2 = z2.copy()
+    lc1 = len(z1)
+    lc2 = len(z2)
+    if lc2 == 1:
+        z1 /= z2
+        return z1, z1[:1]*0
+    elif lc1 < lc2:
+        return z1[:1]*0, z1
+    else:
+        dlen = lc1 - lc2
+        scl = z2[0]
+        z2 /= scl
+        quo = np.empty(dlen + 1, dtype=z1.dtype)
+        i = 0
+        j = dlen
+        while i < j:
+            r = z1[i]
+            quo[i] = z1[i]
+            quo[dlen - i] = r
+            tmp = r*z2
+            z1[i:i+lc2] -= tmp
+            z1[j:j+lc2] -= tmp
+            i += 1
+            j -= 1
+        r = z1[i]
+        quo[i] = r
+        tmp = r*z2
+        z1[i:i+lc2] -= tmp
+        quo /= scl
+        rem = z1[i+1:i-1+lc2].copy()
+        return quo, rem
+
+
+def _zseries_der(zs):
+    """Differentiate a z-series.
+
+    The derivative is with respect to x, not z. This is achieved using the
+    chain rule and the value of dx/dz given in the module notes.
+
+    Parameters
+    ----------
+    zs : z-series
+        The z-series to differentiate.
+
+    Returns
+    -------
+    derivative : z-series
+        The derivative
+
+    Notes
+    -----
+    The zseries for x (ns) has been multiplied by two in order to avoid
+    using floats that are incompatible with Decimal and likely other
+    specialized scalar types. This scaling has been compensated by
+    multiplying the value of zs by two also so that the two cancels in the
+    division.
+
+    """
+    n = len(zs)//2
+    ns = np.array([-1, 0, 1], dtype=zs.dtype)
+    zs *= np.arange(-n, n+1)*2
+    d, r = _zseries_div(zs, ns)
+    return d
+
+
+def _zseries_int(zs):
+    """Integrate a z-series.
+
+    The integral is with respect to x, not z. This is achieved by a change
+    of variable using dx/dz given in the module notes.
+
+    Parameters
+    ----------
+    zs : z-series
+        The z-series to integrate
+
+    Returns
+    -------
+    integral : z-series
+        The indefinite integral
+
+    Notes
+    -----
+    The zseries for x (ns) has been multiplied by two in order to avoid
+    using floats that are incompatible with Decimal and likely other
+    specialized scalar types. This scaling has been compensated by
+    dividing the resulting zs by two.
+
+    """
+    n = 1 + len(zs)//2
+    ns = np.array([-1, 0, 1], dtype=zs.dtype)
+    zs = _zseries_mul(zs, ns)
+    div = np.arange(-n, n+1)*2
+    zs[:n] /= div[:n]
+    zs[n+1:] /= div[n+1:]
+    zs[n] = 0
+    return zs
+
+#
+# Chebyshev series functions
+#
+
+
+def poly2cheb(pol):
+    """
+    Convert a polynomial to a Chebyshev series.
+
+    Convert an array representing the coefficients of a polynomial (relative
+    to the "standard" basis) ordered from lowest degree to highest, to an
+    array of the coefficients of the equivalent Chebyshev series, ordered
+    from lowest to highest degree.
+
+    Parameters
+    ----------
+    pol : array_like
+        1-D array containing the polynomial coefficients
+
+    Returns
+    -------
+    c : ndarray
+        1-D array containing the coefficients of the equivalent Chebyshev
+        series.
+
+    See Also
+    --------
+    cheb2poly
+
+    Notes
+    -----
+    The easy way to do conversions between polynomial basis sets
+    is to use the convert method of a class instance.
+
+    Examples
+    --------
+    >>> from numpy import polynomial as P
+    >>> p = P.Polynomial(range(4))
+    >>> p
+    Polynomial([0., 1., 2., 3.], domain=[-1,  1], window=[-1,  1])
+    >>> c = p.convert(kind=P.Chebyshev)
+    >>> c
+    Chebyshev([1.  , 3.25, 1.  , 0.75], domain=[-1.,  1.], window=[-1.,  1.])
+    >>> P.chebyshev.poly2cheb(range(4))
+    array([1.  , 3.25, 1.  , 0.75])
+
+    """
+    [pol] = pu.as_series([pol])
+    deg = len(pol) - 1
+    res = 0
+    for i in range(deg, -1, -1):
+        res = chebadd(chebmulx(res), pol[i])
+    return res
+
+
+def cheb2poly(c):
+    """
+    Convert a Chebyshev series to a polynomial.
+
+    Convert an array representing the coefficients of a Chebyshev series,
+    ordered from lowest degree to highest, to an array of the coefficients
+    of the equivalent polynomial (relative to the "standard" basis) ordered
+    from lowest to highest degree.
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array containing the Chebyshev series coefficients, ordered
+        from lowest order term to highest.
+
+    Returns
+    -------
+    pol : ndarray
+        1-D array containing the coefficients of the equivalent polynomial
+        (relative to the "standard" basis) ordered from lowest order term
+        to highest.
+
+    See Also
+    --------
+    poly2cheb
+
+    Notes
+    -----
+    The easy way to do conversions between polynomial basis sets
+    is to use the convert method of a class instance.
+
+    Examples
+    --------
+    >>> from numpy import polynomial as P
+    >>> c = P.Chebyshev(range(4))
+    >>> c
+    Chebyshev([0., 1., 2., 3.], domain=[-1,  1], window=[-1,  1])
+    >>> p = c.convert(kind=P.Polynomial)
+    >>> p
+    Polynomial([-2., -8.,  4., 12.], domain=[-1.,  1.], window=[-1.,  1.])
+    >>> P.chebyshev.cheb2poly(range(4))
+    array([-2.,  -8.,   4.,  12.])
+
+    """
+    from .polynomial import polyadd, polysub, polymulx
+
+    [c] = pu.as_series([c])
+    n = len(c)
+    if n < 3:
+        return c
+    else:
+        c0 = c[-2]
+        c1 = c[-1]
+        # i is the current degree of c1
+        for i in range(n - 1, 1, -1):
+            tmp = c0
+            c0 = polysub(c[i - 2], c1)
+            c1 = polyadd(tmp, polymulx(c1)*2)
+        return polyadd(c0, polymulx(c1))
+
+
+#
+# These are constant arrays are of integer type so as to be compatible
+# with the widest range of other types, such as Decimal.
+#
+
+# Chebyshev default domain.
+chebdomain = np.array([-1, 1])
+
+# Chebyshev coefficients representing zero.
+chebzero = np.array([0])
+
+# Chebyshev coefficients representing one.
+chebone = np.array([1])
+
+# Chebyshev coefficients representing the identity x.
+chebx = np.array([0, 1])
+
+
+def chebline(off, scl):
+    """
+    Chebyshev series whose graph is a straight line.
+
+    Parameters
+    ----------
+    off, scl : scalars
+        The specified line is given by ``off + scl*x``.
+
+    Returns
+    -------
+    y : ndarray
+        This module's representation of the Chebyshev series for
+        ``off + scl*x``.
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyline
+    numpy.polynomial.legendre.legline
+    numpy.polynomial.laguerre.lagline
+    numpy.polynomial.hermite.hermline
+    numpy.polynomial.hermite_e.hermeline
+
+    Examples
+    --------
+    >>> import numpy.polynomial.chebyshev as C
+    >>> C.chebline(3,2)
+    array([3, 2])
+    >>> C.chebval(-3, C.chebline(3,2)) # should be -3
+    -3.0
+
+    """
+    if scl != 0:
+        return np.array([off, scl])
+    else:
+        return np.array([off])
+
+
+def chebfromroots(roots):
+    """
+    Generate a Chebyshev series with given roots.
+
+    The function returns the coefficients of the polynomial
+
+    .. math:: p(x) = (x - r_0) * (x - r_1) * ... * (x - r_n),
+
+    in Chebyshev form, where the `r_n` are the roots specified in `roots`.
+    If a zero has multiplicity n, then it must appear in `roots` n times.
+    For instance, if 2 is a root of multiplicity three and 3 is a root of
+    multiplicity 2, then `roots` looks something like [2, 2, 2, 3, 3]. The
+    roots can appear in any order.
+
+    If the returned coefficients are `c`, then
+
+    .. math:: p(x) = c_0 + c_1 * T_1(x) + ... +  c_n * T_n(x)
+
+    The coefficient of the last term is not generally 1 for monic
+    polynomials in Chebyshev form.
+
+    Parameters
+    ----------
+    roots : array_like
+        Sequence containing the roots.
+
+    Returns
+    -------
+    out : ndarray
+        1-D array of coefficients.  If all roots are real then `out` is a
+        real array, if some of the roots are complex, then `out` is complex
+        even if all the coefficients in the result are real (see Examples
+        below).
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyfromroots
+    numpy.polynomial.legendre.legfromroots
+    numpy.polynomial.laguerre.lagfromroots
+    numpy.polynomial.hermite.hermfromroots
+    numpy.polynomial.hermite_e.hermefromroots
+
+    Examples
+    --------
+    >>> import numpy.polynomial.chebyshev as C
+    >>> C.chebfromroots((-1,0,1)) # x^3 - x relative to the standard basis
+    array([ 0.  , -0.25,  0.  ,  0.25])
+    >>> j = complex(0,1)
+    >>> C.chebfromroots((-j,j)) # x^2 + 1 relative to the standard basis
+    array([1.5+0.j, 0. +0.j, 0.5+0.j])
+
+    """
+    return pu._fromroots(chebline, chebmul, roots)
+
+
+def chebadd(c1, c2):
+    """
+    Add one Chebyshev series to another.
+
+    Returns the sum of two Chebyshev series `c1` + `c2`.  The arguments
+    are sequences of coefficients ordered from lowest order term to
+    highest, i.e., [1,2,3] represents the series ``T_0 + 2*T_1 + 3*T_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Chebyshev series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Array representing the Chebyshev series of their sum.
+
+    See Also
+    --------
+    chebsub, chebmulx, chebmul, chebdiv, chebpow
+
+    Notes
+    -----
+    Unlike multiplication, division, etc., the sum of two Chebyshev series
+    is a Chebyshev series (without having to "reproject" the result onto
+    the basis set) so addition, just like that of "standard" polynomials,
+    is simply "component-wise."
+
+    Examples
+    --------
+    >>> from numpy.polynomial import chebyshev as C
+    >>> c1 = (1,2,3)
+    >>> c2 = (3,2,1)
+    >>> C.chebadd(c1,c2)
+    array([4., 4., 4.])
+
+    """
+    return pu._add(c1, c2)
+
+
+def chebsub(c1, c2):
+    """
+    Subtract one Chebyshev series from another.
+
+    Returns the difference of two Chebyshev series `c1` - `c2`.  The
+    sequences of coefficients are from lowest order term to highest, i.e.,
+    [1,2,3] represents the series ``T_0 + 2*T_1 + 3*T_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Chebyshev series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Of Chebyshev series coefficients representing their difference.
+
+    See Also
+    --------
+    chebadd, chebmulx, chebmul, chebdiv, chebpow
+
+    Notes
+    -----
+    Unlike multiplication, division, etc., the difference of two Chebyshev
+    series is a Chebyshev series (without having to "reproject" the result
+    onto the basis set) so subtraction, just like that of "standard"
+    polynomials, is simply "component-wise."
+
+    Examples
+    --------
+    >>> from numpy.polynomial import chebyshev as C
+    >>> c1 = (1,2,3)
+    >>> c2 = (3,2,1)
+    >>> C.chebsub(c1,c2)
+    array([-2.,  0.,  2.])
+    >>> C.chebsub(c2,c1) # -C.chebsub(c1,c2)
+    array([ 2.,  0., -2.])
+
+    """
+    return pu._sub(c1, c2)
+
+
+def chebmulx(c):
+    """Multiply a Chebyshev series by x.
+
+    Multiply the polynomial `c` by x, where x is the independent
+    variable.
+
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Chebyshev series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Array representing the result of the multiplication.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.5.0
+
+    Examples
+    --------
+    >>> from numpy.polynomial import chebyshev as C
+    >>> C.chebmulx([1,2,3])
+    array([1. , 2.5, 1. , 1.5])
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    # The zero series needs special treatment
+    if len(c) == 1 and c[0] == 0:
+        return c
+
+    prd = np.empty(len(c) + 1, dtype=c.dtype)
+    prd[0] = c[0]*0
+    prd[1] = c[0]
+    if len(c) > 1:
+        tmp = c[1:]/2
+        prd[2:] = tmp
+        prd[0:-2] += tmp
+    return prd
+
+
+def chebmul(c1, c2):
+    """
+    Multiply one Chebyshev series by another.
+
+    Returns the product of two Chebyshev series `c1` * `c2`.  The arguments
+    are sequences of coefficients, from lowest order "term" to highest,
+    e.g., [1,2,3] represents the series ``T_0 + 2*T_1 + 3*T_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Chebyshev series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Of Chebyshev series coefficients representing their product.
+
+    See Also
+    --------
+    chebadd, chebsub, chebmulx, chebdiv, chebpow
+
+    Notes
+    -----
+    In general, the (polynomial) product of two C-series results in terms
+    that are not in the Chebyshev polynomial basis set.  Thus, to express
+    the product as a C-series, it is typically necessary to "reproject"
+    the product onto said basis set, which typically produces
+    "unintuitive live" (but correct) results; see Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial import chebyshev as C
+    >>> c1 = (1,2,3)
+    >>> c2 = (3,2,1)
+    >>> C.chebmul(c1,c2) # multiplication requires "reprojection"
+    array([  6.5,  12. ,  12. ,   4. ,   1.5])
+
+    """
+    # c1, c2 are trimmed copies
+    [c1, c2] = pu.as_series([c1, c2])
+    z1 = _cseries_to_zseries(c1)
+    z2 = _cseries_to_zseries(c2)
+    prd = _zseries_mul(z1, z2)
+    ret = _zseries_to_cseries(prd)
+    return pu.trimseq(ret)
+
+
+def chebdiv(c1, c2):
+    """
+    Divide one Chebyshev series by another.
+
+    Returns the quotient-with-remainder of two Chebyshev series
+    `c1` / `c2`.  The arguments are sequences of coefficients from lowest
+    order "term" to highest, e.g., [1,2,3] represents the series
+    ``T_0 + 2*T_1 + 3*T_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Chebyshev series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    [quo, rem] : ndarrays
+        Of Chebyshev series coefficients representing the quotient and
+        remainder.
+
+    See Also
+    --------
+    chebadd, chebsub, chebmulx, chebmul, chebpow
+
+    Notes
+    -----
+    In general, the (polynomial) division of one C-series by another
+    results in quotient and remainder terms that are not in the Chebyshev
+    polynomial basis set.  Thus, to express these results as C-series, it
+    is typically necessary to "reproject" the results onto said basis
+    set, which typically produces "unintuitive" (but correct) results;
+    see Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial import chebyshev as C
+    >>> c1 = (1,2,3)
+    >>> c2 = (3,2,1)
+    >>> C.chebdiv(c1,c2) # quotient "intuitive," remainder not
+    (array([3.]), array([-8., -4.]))
+    >>> c2 = (0,1,2,3)
+    >>> C.chebdiv(c2,c1) # neither "intuitive"
+    (array([0., 2.]), array([-2., -4.]))
+
+    """
+    # c1, c2 are trimmed copies
+    [c1, c2] = pu.as_series([c1, c2])
+    if c2[-1] == 0:
+        raise ZeroDivisionError()
+
+    # note: this is more efficient than `pu._div(chebmul, c1, c2)`
+    lc1 = len(c1)
+    lc2 = len(c2)
+    if lc1 < lc2:
+        return c1[:1]*0, c1
+    elif lc2 == 1:
+        return c1/c2[-1], c1[:1]*0
+    else:
+        z1 = _cseries_to_zseries(c1)
+        z2 = _cseries_to_zseries(c2)
+        quo, rem = _zseries_div(z1, z2)
+        quo = pu.trimseq(_zseries_to_cseries(quo))
+        rem = pu.trimseq(_zseries_to_cseries(rem))
+        return quo, rem
+
+
+def chebpow(c, pow, maxpower=16):
+    """Raise a Chebyshev series to a power.
+
+    Returns the Chebyshev series `c` raised to the power `pow`. The
+    argument `c` is a sequence of coefficients ordered from low to high.
+    i.e., [1,2,3] is the series  ``T_0 + 2*T_1 + 3*T_2.``
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Chebyshev series coefficients ordered from low to
+        high.
+    pow : integer
+        Power to which the series will be raised
+    maxpower : integer, optional
+        Maximum power allowed. This is mainly to limit growth of the series
+        to unmanageable size. Default is 16
+
+    Returns
+    -------
+    coef : ndarray
+        Chebyshev series of power.
+
+    See Also
+    --------
+    chebadd, chebsub, chebmulx, chebmul, chebdiv
+
+    Examples
+    --------
+    >>> from numpy.polynomial import chebyshev as C
+    >>> C.chebpow([1, 2, 3, 4], 2)
+    array([15.5, 22. , 16. , ..., 12.5, 12. ,  8. ])
+
+    """
+    # note: this is more efficient than `pu._pow(chebmul, c1, c2)`, as it
+    # avoids converting between z and c series repeatedly
+
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    power = int(pow)
+    if power != pow or power < 0:
+        raise ValueError("Power must be a non-negative integer.")
+    elif maxpower is not None and power > maxpower:
+        raise ValueError("Power is too large")
+    elif power == 0:
+        return np.array([1], dtype=c.dtype)
+    elif power == 1:
+        return c
+    else:
+        # This can be made more efficient by using powers of two
+        # in the usual way.
+        zs = _cseries_to_zseries(c)
+        prd = zs
+        for i in range(2, power + 1):
+            prd = np.convolve(prd, zs)
+        return _zseries_to_cseries(prd)
+
+
+def chebder(c, m=1, scl=1, axis=0):
+    """
+    Differentiate a Chebyshev series.
+
+    Returns the Chebyshev series coefficients `c` differentiated `m` times
+    along `axis`.  At each iteration the result is multiplied by `scl` (the
+    scaling factor is for use in a linear change of variable). The argument
+    `c` is an array of coefficients from low to high degree along each
+    axis, e.g., [1,2,3] represents the series ``1*T_0 + 2*T_1 + 3*T_2``
+    while [[1,2],[1,2]] represents ``1*T_0(x)*T_0(y) + 1*T_1(x)*T_0(y) +
+    2*T_0(x)*T_1(y) + 2*T_1(x)*T_1(y)`` if axis=0 is ``x`` and axis=1 is
+    ``y``.
+
+    Parameters
+    ----------
+    c : array_like
+        Array of Chebyshev series coefficients. If c is multidimensional
+        the different axis correspond to different variables with the
+        degree in each axis given by the corresponding index.
+    m : int, optional
+        Number of derivatives taken, must be non-negative. (Default: 1)
+    scl : scalar, optional
+        Each differentiation is multiplied by `scl`.  The end result is
+        multiplication by ``scl**m``.  This is for use in a linear change of
+        variable. (Default: 1)
+    axis : int, optional
+        Axis over which the derivative is taken. (Default: 0).
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    der : ndarray
+        Chebyshev series of the derivative.
+
+    See Also
+    --------
+    chebint
+
+    Notes
+    -----
+    In general, the result of differentiating a C-series needs to be
+    "reprojected" onto the C-series basis set. Thus, typically, the
+    result of this function is "unintuitive," albeit correct; see Examples
+    section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial import chebyshev as C
+    >>> c = (1,2,3,4)
+    >>> C.chebder(c)
+    array([14., 12., 24.])
+    >>> C.chebder(c,3)
+    array([96.])
+    >>> C.chebder(c,scl=-1)
+    array([-14., -12., -24.])
+    >>> C.chebder(c,2,-1)
+    array([12.,  96.])
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    cnt = pu._deprecate_as_int(m, "the order of derivation")
+    iaxis = pu._deprecate_as_int(axis, "the axis")
+    if cnt < 0:
+        raise ValueError("The order of derivation must be non-negative")
+    iaxis = normalize_axis_index(iaxis, c.ndim)
+
+    if cnt == 0:
+        return c
+
+    c = np.moveaxis(c, iaxis, 0)
+    n = len(c)
+    if cnt >= n:
+        c = c[:1]*0
+    else:
+        for i in range(cnt):
+            n = n - 1
+            c *= scl
+            der = np.empty((n,) + c.shape[1:], dtype=c.dtype)
+            for j in range(n, 2, -1):
+                der[j - 1] = (2*j)*c[j]
+                c[j - 2] += (j*c[j])/(j - 2)
+            if n > 1:
+                der[1] = 4*c[2]
+            der[0] = c[1]
+            c = der
+    c = np.moveaxis(c, 0, iaxis)
+    return c
+
+
+def chebint(c, m=1, k=[], lbnd=0, scl=1, axis=0):
+    """
+    Integrate a Chebyshev series.
+
+    Returns the Chebyshev series coefficients `c` integrated `m` times from
+    `lbnd` along `axis`. At each iteration the resulting series is
+    **multiplied** by `scl` and an integration constant, `k`, is added.
+    The scaling factor is for use in a linear change of variable.  ("Buyer
+    beware": note that, depending on what one is doing, one may want `scl`
+    to be the reciprocal of what one might expect; for more information,
+    see the Notes section below.)  The argument `c` is an array of
+    coefficients from low to high degree along each axis, e.g., [1,2,3]
+    represents the series ``T_0 + 2*T_1 + 3*T_2`` while [[1,2],[1,2]]
+    represents ``1*T_0(x)*T_0(y) + 1*T_1(x)*T_0(y) + 2*T_0(x)*T_1(y) +
+    2*T_1(x)*T_1(y)`` if axis=0 is ``x`` and axis=1 is ``y``.
+
+    Parameters
+    ----------
+    c : array_like
+        Array of Chebyshev series coefficients. If c is multidimensional
+        the different axis correspond to different variables with the
+        degree in each axis given by the corresponding index.
+    m : int, optional
+        Order of integration, must be positive. (Default: 1)
+    k : {[], list, scalar}, optional
+        Integration constant(s).  The value of the first integral at zero
+        is the first value in the list, the value of the second integral
+        at zero is the second value, etc.  If ``k == []`` (the default),
+        all constants are set to zero.  If ``m == 1``, a single scalar can
+        be given instead of a list.
+    lbnd : scalar, optional
+        The lower bound of the integral. (Default: 0)
+    scl : scalar, optional
+        Following each integration the result is *multiplied* by `scl`
+        before the integration constant is added. (Default: 1)
+    axis : int, optional
+        Axis over which the integral is taken. (Default: 0).
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    S : ndarray
+        C-series coefficients of the integral.
+
+    Raises
+    ------
+    ValueError
+        If ``m < 1``, ``len(k) > m``, ``np.ndim(lbnd) != 0``, or
+        ``np.ndim(scl) != 0``.
+
+    See Also
+    --------
+    chebder
+
+    Notes
+    -----
+    Note that the result of each integration is *multiplied* by `scl`.
+    Why is this important to note?  Say one is making a linear change of
+    variable :math:`u = ax + b` in an integral relative to `x`.  Then
+    :math:`dx = du/a`, so one will need to set `scl` equal to
+    :math:`1/a`- perhaps not what one would have first thought.
+
+    Also note that, in general, the result of integrating a C-series needs
+    to be "reprojected" onto the C-series basis set.  Thus, typically,
+    the result of this function is "unintuitive," albeit correct; see
+    Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial import chebyshev as C
+    >>> c = (1,2,3)
+    >>> C.chebint(c)
+    array([ 0.5, -0.5,  0.5,  0.5])
+    >>> C.chebint(c,3)
+    array([ 0.03125   , -0.1875    ,  0.04166667, -0.05208333,  0.01041667, # may vary
+        0.00625   ])
+    >>> C.chebint(c, k=3)
+    array([ 3.5, -0.5,  0.5,  0.5])
+    >>> C.chebint(c,lbnd=-2)
+    array([ 8.5, -0.5,  0.5,  0.5])
+    >>> C.chebint(c,scl=-2)
+    array([-1.,  1., -1., -1.])
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    if not np.iterable(k):
+        k = [k]
+    cnt = pu._deprecate_as_int(m, "the order of integration")
+    iaxis = pu._deprecate_as_int(axis, "the axis")
+    if cnt < 0:
+        raise ValueError("The order of integration must be non-negative")
+    if len(k) > cnt:
+        raise ValueError("Too many integration constants")
+    if np.ndim(lbnd) != 0:
+        raise ValueError("lbnd must be a scalar.")
+    if np.ndim(scl) != 0:
+        raise ValueError("scl must be a scalar.")
+    iaxis = normalize_axis_index(iaxis, c.ndim)
+
+    if cnt == 0:
+        return c
+
+    c = np.moveaxis(c, iaxis, 0)
+    k = list(k) + [0]*(cnt - len(k))
+    for i in range(cnt):
+        n = len(c)
+        c *= scl
+        if n == 1 and np.all(c[0] == 0):
+            c[0] += k[i]
+        else:
+            tmp = np.empty((n + 1,) + c.shape[1:], dtype=c.dtype)
+            tmp[0] = c[0]*0
+            tmp[1] = c[0]
+            if n > 1:
+                tmp[2] = c[1]/4
+            for j in range(2, n):
+                tmp[j + 1] = c[j]/(2*(j + 1))
+                tmp[j - 1] -= c[j]/(2*(j - 1))
+            tmp[0] += k[i] - chebval(lbnd, tmp)
+            c = tmp
+    c = np.moveaxis(c, 0, iaxis)
+    return c
+
+
+def chebval(x, c, tensor=True):
+    """
+    Evaluate a Chebyshev series at points x.
+
+    If `c` is of length `n + 1`, this function returns the value:
+
+    .. math:: p(x) = c_0 * T_0(x) + c_1 * T_1(x) + ... + c_n * T_n(x)
+
+    The parameter `x` is converted to an array only if it is a tuple or a
+    list, otherwise it is treated as a scalar. In either case, either `x`
+    or its elements must support multiplication and addition both with
+    themselves and with the elements of `c`.
+
+    If `c` is a 1-D array, then `p(x)` will have the same shape as `x`.  If
+    `c` is multidimensional, then the shape of the result depends on the
+    value of `tensor`. If `tensor` is true the shape will be c.shape[1:] +
+    x.shape. If `tensor` is false the shape will be c.shape[1:]. Note that
+    scalars have shape (,).
+
+    Trailing zeros in the coefficients will be used in the evaluation, so
+    they should be avoided if efficiency is a concern.
+
+    Parameters
+    ----------
+    x : array_like, compatible object
+        If `x` is a list or tuple, it is converted to an ndarray, otherwise
+        it is left unchanged and treated as a scalar. In either case, `x`
+        or its elements must support addition and multiplication with
+        themselves and with the elements of `c`.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree n are contained in c[n]. If `c` is multidimensional the
+        remaining indices enumerate multiple polynomials. In the two
+        dimensional case the coefficients may be thought of as stored in
+        the columns of `c`.
+    tensor : boolean, optional
+        If True, the shape of the coefficient array is extended with ones
+        on the right, one for each dimension of `x`. Scalars have dimension 0
+        for this action. The result is that every column of coefficients in
+        `c` is evaluated for every element of `x`. If False, `x` is broadcast
+        over the columns of `c` for the evaluation.  This keyword is useful
+        when `c` is multidimensional. The default value is True.
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    values : ndarray, algebra_like
+        The shape of the return value is described above.
+
+    See Also
+    --------
+    chebval2d, chebgrid2d, chebval3d, chebgrid3d
+
+    Notes
+    -----
+    The evaluation uses Clenshaw recursion, aka synthetic division.
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    if isinstance(x, (tuple, list)):
+        x = np.asarray(x)
+    if isinstance(x, np.ndarray) and tensor:
+        c = c.reshape(c.shape + (1,)*x.ndim)
+
+    if len(c) == 1:
+        c0 = c[0]
+        c1 = 0
+    elif len(c) == 2:
+        c0 = c[0]
+        c1 = c[1]
+    else:
+        x2 = 2*x
+        c0 = c[-2]
+        c1 = c[-1]
+        for i in range(3, len(c) + 1):
+            tmp = c0
+            c0 = c[-i] - c1
+            c1 = tmp + c1*x2
+    return c0 + c1*x
+
+
+def chebval2d(x, y, c):
+    """
+    Evaluate a 2-D Chebyshev series at points (x, y).
+
+    This function returns the values:
+
+    .. math:: p(x,y) = \\sum_{i,j} c_{i,j} * T_i(x) * T_j(y)
+
+    The parameters `x` and `y` are converted to arrays only if they are
+    tuples or a lists, otherwise they are treated as a scalars and they
+    must have the same shape after conversion. In either case, either `x`
+    and `y` or their elements must support multiplication and addition both
+    with themselves and with the elements of `c`.
+
+    If `c` is a 1-D array a one is implicitly appended to its shape to make
+    it 2-D. The shape of the result will be c.shape[2:] + x.shape.
+
+    Parameters
+    ----------
+    x, y : array_like, compatible objects
+        The two dimensional series is evaluated at the points `(x, y)`,
+        where `x` and `y` must have the same shape. If `x` or `y` is a list
+        or tuple, it is first converted to an ndarray, otherwise it is left
+        unchanged and if it isn't an ndarray it is treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term
+        of multi-degree i,j is contained in ``c[i,j]``. If `c` has
+        dimension greater than 2 the remaining indices enumerate multiple
+        sets of coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional Chebyshev series at points formed
+        from pairs of corresponding values from `x` and `y`.
+
+    See Also
+    --------
+    chebval, chebgrid2d, chebval3d, chebgrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._valnd(chebval, c, x, y)
+
+
+def chebgrid2d(x, y, c):
+    """
+    Evaluate a 2-D Chebyshev series on the Cartesian product of x and y.
+
+    This function returns the values:
+
+    .. math:: p(a,b) = \\sum_{i,j} c_{i,j} * T_i(a) * T_j(b),
+
+    where the points `(a, b)` consist of all pairs formed by taking
+    `a` from `x` and `b` from `y`. The resulting points form a grid with
+    `x` in the first dimension and `y` in the second.
+
+    The parameters `x` and `y` are converted to arrays only if they are
+    tuples or a lists, otherwise they are treated as a scalars. In either
+    case, either `x` and `y` or their elements must support multiplication
+    and addition both with themselves and with the elements of `c`.
+
+    If `c` has fewer than two dimensions, ones are implicitly appended to
+    its shape to make it 2-D. The shape of the result will be c.shape[2:] +
+    x.shape + y.shape.
+
+    Parameters
+    ----------
+    x, y : array_like, compatible objects
+        The two dimensional series is evaluated at the points in the
+        Cartesian product of `x` and `y`.  If `x` or `y` is a list or
+        tuple, it is first converted to an ndarray, otherwise it is left
+        unchanged and, if it isn't an ndarray, it is treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term of
+        multi-degree i,j is contained in `c[i,j]`. If `c` has dimension
+        greater than two the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional Chebyshev series at points in the
+        Cartesian product of `x` and `y`.
+
+    See Also
+    --------
+    chebval, chebval2d, chebval3d, chebgrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._gridnd(chebval, c, x, y)
+
+
+def chebval3d(x, y, z, c):
+    """
+    Evaluate a 3-D Chebyshev series at points (x, y, z).
+
+    This function returns the values:
+
+    .. math:: p(x,y,z) = \\sum_{i,j,k} c_{i,j,k} * T_i(x) * T_j(y) * T_k(z)
+
+    The parameters `x`, `y`, and `z` are converted to arrays only if
+    they are tuples or a lists, otherwise they are treated as a scalars and
+    they must have the same shape after conversion. In either case, either
+    `x`, `y`, and `z` or their elements must support multiplication and
+    addition both with themselves and with the elements of `c`.
+
+    If `c` has fewer than 3 dimensions, ones are implicitly appended to its
+    shape to make it 3-D. The shape of the result will be c.shape[3:] +
+    x.shape.
+
+    Parameters
+    ----------
+    x, y, z : array_like, compatible object
+        The three dimensional series is evaluated at the points
+        `(x, y, z)`, where `x`, `y`, and `z` must have the same shape.  If
+        any of `x`, `y`, or `z` is a list or tuple, it is first converted
+        to an ndarray, otherwise it is left unchanged and if it isn't an
+        ndarray it is  treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term of
+        multi-degree i,j,k is contained in ``c[i,j,k]``. If `c` has dimension
+        greater than 3 the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the multidimensional polynomial on points formed with
+        triples of corresponding values from `x`, `y`, and `z`.
+
+    See Also
+    --------
+    chebval, chebval2d, chebgrid2d, chebgrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._valnd(chebval, c, x, y, z)
+
+
+def chebgrid3d(x, y, z, c):
+    """
+    Evaluate a 3-D Chebyshev series on the Cartesian product of x, y, and z.
+
+    This function returns the values:
+
+    .. math:: p(a,b,c) = \\sum_{i,j,k} c_{i,j,k} * T_i(a) * T_j(b) * T_k(c)
+
+    where the points `(a, b, c)` consist of all triples formed by taking
+    `a` from `x`, `b` from `y`, and `c` from `z`. The resulting points form
+    a grid with `x` in the first dimension, `y` in the second, and `z` in
+    the third.
+
+    The parameters `x`, `y`, and `z` are converted to arrays only if they
+    are tuples or a lists, otherwise they are treated as a scalars. In
+    either case, either `x`, `y`, and `z` or their elements must support
+    multiplication and addition both with themselves and with the elements
+    of `c`.
+
+    If `c` has fewer than three dimensions, ones are implicitly appended to
+    its shape to make it 3-D. The shape of the result will be c.shape[3:] +
+    x.shape + y.shape + z.shape.
+
+    Parameters
+    ----------
+    x, y, z : array_like, compatible objects
+        The three dimensional series is evaluated at the points in the
+        Cartesian product of `x`, `y`, and `z`.  If `x`,`y`, or `z` is a
+        list or tuple, it is first converted to an ndarray, otherwise it is
+        left unchanged and, if it isn't an ndarray, it is treated as a
+        scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree i,j are contained in ``c[i,j]``. If `c` has dimension
+        greater than two the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points in the Cartesian
+        product of `x` and `y`.
+
+    See Also
+    --------
+    chebval, chebval2d, chebgrid2d, chebval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._gridnd(chebval, c, x, y, z)
+
+
+def chebvander(x, deg):
+    """Pseudo-Vandermonde matrix of given degree.
+
+    Returns the pseudo-Vandermonde matrix of degree `deg` and sample points
+    `x`. The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., i] = T_i(x),
+
+    where `0 <= i <= deg`. The leading indices of `V` index the elements of
+    `x` and the last index is the degree of the Chebyshev polynomial.
+
+    If `c` is a 1-D array of coefficients of length `n + 1` and `V` is the
+    matrix ``V = chebvander(x, n)``, then ``np.dot(V, c)`` and
+    ``chebval(x, c)`` are the same up to roundoff.  This equivalence is
+    useful both for least squares fitting and for the evaluation of a large
+    number of Chebyshev series of the same degree and sample points.
+
+    Parameters
+    ----------
+    x : array_like
+        Array of points. The dtype is converted to float64 or complex128
+        depending on whether any of the elements are complex. If `x` is
+        scalar it is converted to a 1-D array.
+    deg : int
+        Degree of the resulting matrix.
+
+    Returns
+    -------
+    vander : ndarray
+        The pseudo Vandermonde matrix. The shape of the returned matrix is
+        ``x.shape + (deg + 1,)``, where The last index is the degree of the
+        corresponding Chebyshev polynomial.  The dtype will be the same as
+        the converted `x`.
+
+    """
+    ideg = pu._deprecate_as_int(deg, "deg")
+    if ideg < 0:
+        raise ValueError("deg must be non-negative")
+
+    x = np.array(x, copy=False, ndmin=1) + 0.0
+    dims = (ideg + 1,) + x.shape
+    dtyp = x.dtype
+    v = np.empty(dims, dtype=dtyp)
+    # Use forward recursion to generate the entries.
+    v[0] = x*0 + 1
+    if ideg > 0:
+        x2 = 2*x
+        v[1] = x
+        for i in range(2, ideg + 1):
+            v[i] = v[i-1]*x2 - v[i-2]
+    return np.moveaxis(v, 0, -1)
+
+
+def chebvander2d(x, y, deg):
+    """Pseudo-Vandermonde matrix of given degrees.
+
+    Returns the pseudo-Vandermonde matrix of degrees `deg` and sample
+    points `(x, y)`. The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., (deg[1] + 1)*i + j] = T_i(x) * T_j(y),
+
+    where `0 <= i <= deg[0]` and `0 <= j <= deg[1]`. The leading indices of
+    `V` index the points `(x, y)` and the last index encodes the degrees of
+    the Chebyshev polynomials.
+
+    If ``V = chebvander2d(x, y, [xdeg, ydeg])``, then the columns of `V`
+    correspond to the elements of a 2-D coefficient array `c` of shape
+    (xdeg + 1, ydeg + 1) in the order
+
+    .. math:: c_{00}, c_{01}, c_{02} ... , c_{10}, c_{11}, c_{12} ...
+
+    and ``np.dot(V, c.flat)`` and ``chebval2d(x, y, c)`` will be the same
+    up to roundoff. This equivalence is useful both for least squares
+    fitting and for the evaluation of a large number of 2-D Chebyshev
+    series of the same degrees and sample points.
+
+    Parameters
+    ----------
+    x, y : array_like
+        Arrays of point coordinates, all of the same shape. The dtypes
+        will be converted to either float64 or complex128 depending on
+        whether any of the elements are complex. Scalars are converted to
+        1-D arrays.
+    deg : list of ints
+        List of maximum degrees of the form [x_deg, y_deg].
+
+    Returns
+    -------
+    vander2d : ndarray
+        The shape of the returned matrix is ``x.shape + (order,)``, where
+        :math:`order = (deg[0]+1)*(deg[1]+1)`.  The dtype will be the same
+        as the converted `x` and `y`.
+
+    See Also
+    --------
+    chebvander, chebvander3d, chebval2d, chebval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._vander_nd_flat((chebvander, chebvander), (x, y), deg)
+
+
+def chebvander3d(x, y, z, deg):
+    """Pseudo-Vandermonde matrix of given degrees.
+
+    Returns the pseudo-Vandermonde matrix of degrees `deg` and sample
+    points `(x, y, z)`. If `l, m, n` are the given degrees in `x, y, z`,
+    then The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., (m+1)(n+1)i + (n+1)j + k] = T_i(x)*T_j(y)*T_k(z),
+
+    where `0 <= i <= l`, `0 <= j <= m`, and `0 <= j <= n`.  The leading
+    indices of `V` index the points `(x, y, z)` and the last index encodes
+    the degrees of the Chebyshev polynomials.
+
+    If ``V = chebvander3d(x, y, z, [xdeg, ydeg, zdeg])``, then the columns
+    of `V` correspond to the elements of a 3-D coefficient array `c` of
+    shape (xdeg + 1, ydeg + 1, zdeg + 1) in the order
+
+    .. math:: c_{000}, c_{001}, c_{002},... , c_{010}, c_{011}, c_{012},...
+
+    and ``np.dot(V, c.flat)`` and ``chebval3d(x, y, z, c)`` will be the
+    same up to roundoff. This equivalence is useful both for least squares
+    fitting and for the evaluation of a large number of 3-D Chebyshev
+    series of the same degrees and sample points.
+
+    Parameters
+    ----------
+    x, y, z : array_like
+        Arrays of point coordinates, all of the same shape. The dtypes will
+        be converted to either float64 or complex128 depending on whether
+        any of the elements are complex. Scalars are converted to 1-D
+        arrays.
+    deg : list of ints
+        List of maximum degrees of the form [x_deg, y_deg, z_deg].
+
+    Returns
+    -------
+    vander3d : ndarray
+        The shape of the returned matrix is ``x.shape + (order,)``, where
+        :math:`order = (deg[0]+1)*(deg[1]+1)*(deg[2]+1)`.  The dtype will
+        be the same as the converted `x`, `y`, and `z`.
+
+    See Also
+    --------
+    chebvander, chebvander3d, chebval2d, chebval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._vander_nd_flat((chebvander, chebvander, chebvander), (x, y, z), deg)
+
+
+def chebfit(x, y, deg, rcond=None, full=False, w=None):
+    """
+    Least squares fit of Chebyshev series to data.
+
+    Return the coefficients of a Chebyshev series of degree `deg` that is the
+    least squares fit to the data values `y` given at points `x`. If `y` is
+    1-D the returned coefficients will also be 1-D. If `y` is 2-D multiple
+    fits are done, one for each column of `y`, and the resulting
+    coefficients are stored in the corresponding columns of a 2-D return.
+    The fitted polynomial(s) are in the form
+
+    .. math::  p(x) = c_0 + c_1 * T_1(x) + ... + c_n * T_n(x),
+
+    where `n` is `deg`.
+
+    Parameters
+    ----------
+    x : array_like, shape (M,)
+        x-coordinates of the M sample points ``(x[i], y[i])``.
+    y : array_like, shape (M,) or (M, K)
+        y-coordinates of the sample points. Several data sets of sample
+        points sharing the same x-coordinates can be fitted at once by
+        passing in a 2D-array that contains one dataset per column.
+    deg : int or 1-D array_like
+        Degree(s) of the fitting polynomials. If `deg` is a single integer,
+        all terms up to and including the `deg`'th term are included in the
+        fit. For NumPy versions >= 1.11.0 a list of integers specifying the
+        degrees of the terms to include may be used instead.
+    rcond : float, optional
+        Relative condition number of the fit. Singular values smaller than
+        this relative to the largest singular value will be ignored. The
+        default value is len(x)*eps, where eps is the relative precision of
+        the float type, about 2e-16 in most cases.
+    full : bool, optional
+        Switch determining nature of return value. When it is False (the
+        default) just the coefficients are returned, when True diagnostic
+        information from the singular value decomposition is also returned.
+    w : array_like, shape (`M`,), optional
+        Weights. If not None, the weight ``w[i]`` applies to the unsquared
+        residual ``y[i] - y_hat[i]`` at ``x[i]``. Ideally the weights are
+        chosen so that the errors of the products ``w[i]*y[i]`` all have the
+        same variance.  When using inverse-variance weighting, use
+        ``w[i] = 1/sigma(y[i])``.  The default value is None.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    coef : ndarray, shape (M,) or (M, K)
+        Chebyshev coefficients ordered from low to high. If `y` was 2-D,
+        the coefficients for the data in column k  of `y` are in column
+        `k`.
+
+    [residuals, rank, singular_values, rcond] : list
+        These values are only returned if ``full == True``
+
+        - residuals -- sum of squared residuals of the least squares fit
+        - rank -- the numerical rank of the scaled Vandermonde matrix
+        - singular_values -- singular values of the scaled Vandermonde matrix
+        - rcond -- value of `rcond`.
+
+        For more details, see `numpy.linalg.lstsq`.
+
+    Warns
+    -----
+    RankWarning
+        The rank of the coefficient matrix in the least-squares fit is
+        deficient. The warning is only raised if ``full == False``.  The
+        warnings can be turned off by
+
+        >>> import warnings
+        >>> warnings.simplefilter('ignore', np.RankWarning)
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyfit
+    numpy.polynomial.legendre.legfit
+    numpy.polynomial.laguerre.lagfit
+    numpy.polynomial.hermite.hermfit
+    numpy.polynomial.hermite_e.hermefit
+    chebval : Evaluates a Chebyshev series.
+    chebvander : Vandermonde matrix of Chebyshev series.
+    chebweight : Chebyshev weight function.
+    numpy.linalg.lstsq : Computes a least-squares fit from the matrix.
+    scipy.interpolate.UnivariateSpline : Computes spline fits.
+
+    Notes
+    -----
+    The solution is the coefficients of the Chebyshev series `p` that
+    minimizes the sum of the weighted squared errors
+
+    .. math:: E = \\sum_j w_j^2 * |y_j - p(x_j)|^2,
+
+    where :math:`w_j` are the weights. This problem is solved by setting up
+    as the (typically) overdetermined matrix equation
+
+    .. math:: V(x) * c = w * y,
+
+    where `V` is the weighted pseudo Vandermonde matrix of `x`, `c` are the
+    coefficients to be solved for, `w` are the weights, and `y` are the
+    observed values.  This equation is then solved using the singular value
+    decomposition of `V`.
+
+    If some of the singular values of `V` are so small that they are
+    neglected, then a `RankWarning` will be issued. This means that the
+    coefficient values may be poorly determined. Using a lower order fit
+    will usually get rid of the warning.  The `rcond` parameter can also be
+    set to a value smaller than its default, but the resulting fit may be
+    spurious and have large contributions from roundoff error.
+
+    Fits using Chebyshev series are usually better conditioned than fits
+    using power series, but much can depend on the distribution of the
+    sample points and the smoothness of the data. If the quality of the fit
+    is inadequate splines may be a good alternative.
+
+    References
+    ----------
+    .. [1] Wikipedia, "Curve fitting",
+           https://en.wikipedia.org/wiki/Curve_fitting
+
+    Examples
+    --------
+
+    """
+    return pu._fit(chebvander, x, y, deg, rcond, full, w)
+
+
+def chebcompanion(c):
+    """Return the scaled companion matrix of c.
+
+    The basis polynomials are scaled so that the companion matrix is
+    symmetric when `c` is a Chebyshev basis polynomial. This provides
+    better eigenvalue estimates than the unscaled case and for basis
+    polynomials the eigenvalues are guaranteed to be real if
+    `numpy.linalg.eigvalsh` is used to obtain them.
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Chebyshev series coefficients ordered from low to high
+        degree.
+
+    Returns
+    -------
+    mat : ndarray
+        Scaled companion matrix of dimensions (deg, deg).
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    if len(c) < 2:
+        raise ValueError('Series must have maximum degree of at least 1.')
+    if len(c) == 2:
+        return np.array([[-c[0]/c[1]]])
+
+    n = len(c) - 1
+    mat = np.zeros((n, n), dtype=c.dtype)
+    scl = np.array([1.] + [np.sqrt(.5)]*(n-1))
+    top = mat.reshape(-1)[1::n+1]
+    bot = mat.reshape(-1)[n::n+1]
+    top[0] = np.sqrt(.5)
+    top[1:] = 1/2
+    bot[...] = top
+    mat[:, -1] -= (c[:-1]/c[-1])*(scl/scl[-1])*.5
+    return mat
+
+
+def chebroots(c):
+    """
+    Compute the roots of a Chebyshev series.
+
+    Return the roots (a.k.a. "zeros") of the polynomial
+
+    .. math:: p(x) = \\sum_i c[i] * T_i(x).
+
+    Parameters
+    ----------
+    c : 1-D array_like
+        1-D array of coefficients.
+
+    Returns
+    -------
+    out : ndarray
+        Array of the roots of the series. If all the roots are real,
+        then `out` is also real, otherwise it is complex.
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyroots
+    numpy.polynomial.legendre.legroots
+    numpy.polynomial.laguerre.lagroots
+    numpy.polynomial.hermite.hermroots
+    numpy.polynomial.hermite_e.hermeroots
+
+    Notes
+    -----
+    The root estimates are obtained as the eigenvalues of the companion
+    matrix, Roots far from the origin of the complex plane may have large
+    errors due to the numerical instability of the series for such
+    values. Roots with multiplicity greater than 1 will also show larger
+    errors as the value of the series near such points is relatively
+    insensitive to errors in the roots. Isolated roots near the origin can
+    be improved by a few iterations of Newton's method.
+
+    The Chebyshev series basis polynomials aren't powers of `x` so the
+    results of this function may seem unintuitive.
+
+    Examples
+    --------
+    >>> import numpy.polynomial.chebyshev as cheb
+    >>> cheb.chebroots((-1, 1,-1, 1)) # T3 - T2 + T1 - T0 has real roots
+    array([ -5.00000000e-01,   2.60860684e-17,   1.00000000e+00]) # may vary
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    if len(c) < 2:
+        return np.array([], dtype=c.dtype)
+    if len(c) == 2:
+        return np.array([-c[0]/c[1]])
+
+    # rotated companion matrix reduces error
+    m = chebcompanion(c)[::-1,::-1]
+    r = la.eigvals(m)
+    r.sort()
+    return r
+
+
+def chebinterpolate(func, deg, args=()):
+    """Interpolate a function at the Chebyshev points of the first kind.
+
+    Returns the Chebyshev series that interpolates `func` at the Chebyshev
+    points of the first kind in the interval [-1, 1]. The interpolating
+    series tends to a minmax approximation to `func` with increasing `deg`
+    if the function is continuous in the interval.
+
+    .. versionadded:: 1.14.0
+
+    Parameters
+    ----------
+    func : function
+        The function to be approximated. It must be a function of a single
+        variable of the form ``f(x, a, b, c...)``, where ``a, b, c...`` are
+        extra arguments passed in the `args` parameter.
+    deg : int
+        Degree of the interpolating polynomial
+    args : tuple, optional
+        Extra arguments to be used in the function call. Default is no extra
+        arguments.
+
+    Returns
+    -------
+    coef : ndarray, shape (deg + 1,)
+        Chebyshev coefficients of the interpolating series ordered from low to
+        high.
+
+    Examples
+    --------
+    >>> import numpy.polynomial.chebyshev as C
+    >>> C.chebfromfunction(lambda x: np.tanh(x) + 0.5, 8)
+    array([  5.00000000e-01,   8.11675684e-01,  -9.86864911e-17,
+            -5.42457905e-02,  -2.71387850e-16,   4.51658839e-03,
+             2.46716228e-17,  -3.79694221e-04,  -3.26899002e-16])
+
+    Notes
+    -----
+
+    The Chebyshev polynomials used in the interpolation are orthogonal when
+    sampled at the Chebyshev points of the first kind. If it is desired to
+    constrain some of the coefficients they can simply be set to the desired
+    value after the interpolation, no new interpolation or fit is needed. This
+    is especially useful if it is known apriori that some of coefficients are
+    zero. For instance, if the function is even then the coefficients of the
+    terms of odd degree in the result can be set to zero.
+
+    """
+    deg = np.asarray(deg)
+
+    # check arguments.
+    if deg.ndim > 0 or deg.dtype.kind not in 'iu' or deg.size == 0:
+        raise TypeError("deg must be an int")
+    if deg < 0:
+        raise ValueError("expected deg >= 0")
+
+    order = deg + 1
+    xcheb = chebpts1(order)
+    yfunc = func(xcheb, *args)
+    m = chebvander(xcheb, deg)
+    c = np.dot(m.T, yfunc)
+    c[0] /= order
+    c[1:] /= 0.5*order
+
+    return c
+
+
+def chebgauss(deg):
+    """
+    Gauss-Chebyshev quadrature.
+
+    Computes the sample points and weights for Gauss-Chebyshev quadrature.
+    These sample points and weights will correctly integrate polynomials of
+    degree :math:`2*deg - 1` or less over the interval :math:`[-1, 1]` with
+    the weight function :math:`f(x) = 1/\\sqrt{1 - x^2}`.
+
+    Parameters
+    ----------
+    deg : int
+        Number of sample points and weights. It must be >= 1.
+
+    Returns
+    -------
+    x : ndarray
+        1-D ndarray containing the sample points.
+    y : ndarray
+        1-D ndarray containing the weights.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    The results have only been tested up to degree 100, higher degrees may
+    be problematic. For Gauss-Chebyshev there are closed form solutions for
+    the sample points and weights. If n = `deg`, then
+
+    .. math:: x_i = \\cos(\\pi (2 i - 1) / (2 n))
+
+    .. math:: w_i = \\pi / n
+
+    """
+    ideg = pu._deprecate_as_int(deg, "deg")
+    if ideg <= 0:
+        raise ValueError("deg must be a positive integer")
+
+    x = np.cos(np.pi * np.arange(1, 2*ideg, 2) / (2.0*ideg))
+    w = np.ones(ideg)*(np.pi/ideg)
+
+    return x, w
+
+
+def chebweight(x):
+    """
+    The weight function of the Chebyshev polynomials.
+
+    The weight function is :math:`1/\\sqrt{1 - x^2}` and the interval of
+    integration is :math:`[-1, 1]`. The Chebyshev polynomials are
+    orthogonal, but not normalized, with respect to this weight function.
+
+    Parameters
+    ----------
+    x : array_like
+       Values at which the weight function will be computed.
+
+    Returns
+    -------
+    w : ndarray
+       The weight function at `x`.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    w = 1./(np.sqrt(1. + x) * np.sqrt(1. - x))
+    return w
+
+
+def chebpts1(npts):
+    """
+    Chebyshev points of the first kind.
+
+    The Chebyshev points of the first kind are the points ``cos(x)``,
+    where ``x = [pi*(k + .5)/npts for k in range(npts)]``.
+
+    Parameters
+    ----------
+    npts : int
+        Number of sample points desired.
+
+    Returns
+    -------
+    pts : ndarray
+        The Chebyshev points of the first kind.
+
+    See Also
+    --------
+    chebpts2
+
+    Notes
+    -----
+
+    .. versionadded:: 1.5.0
+
+    """
+    _npts = int(npts)
+    if _npts != npts:
+        raise ValueError("npts must be integer")
+    if _npts < 1:
+        raise ValueError("npts must be >= 1")
+
+    x = 0.5 * np.pi / _npts * np.arange(-_npts+1, _npts+1, 2)
+    return np.sin(x)
+
+
+def chebpts2(npts):
+    """
+    Chebyshev points of the second kind.
+
+    The Chebyshev points of the second kind are the points ``cos(x)``,
+    where ``x = [pi*k/(npts - 1) for k in range(npts)]`` sorted in ascending
+    order.
+
+    Parameters
+    ----------
+    npts : int
+        Number of sample points desired.
+
+    Returns
+    -------
+    pts : ndarray
+        The Chebyshev points of the second kind.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.5.0
+
+    """
+    _npts = int(npts)
+    if _npts != npts:
+        raise ValueError("npts must be integer")
+    if _npts < 2:
+        raise ValueError("npts must be >= 2")
+
+    x = np.linspace(-np.pi, 0, _npts)
+    return np.cos(x)
+
+
+#
+# Chebyshev series class
+#
+
+class Chebyshev(ABCPolyBase):
+    """A Chebyshev series class.
+
+    The Chebyshev class provides the standard Python numerical methods
+    '+', '-', '*', '//', '%', 'divmod', '**', and '()' as well as the
+    methods listed below.
+
+    Parameters
+    ----------
+    coef : array_like
+        Chebyshev coefficients in order of increasing degree, i.e.,
+        ``(1, 2, 3)`` gives ``1*T_0(x) + 2*T_1(x) + 3*T_2(x)``.
+    domain : (2,) array_like, optional
+        Domain to use. The interval ``[domain[0], domain[1]]`` is mapped
+        to the interval ``[window[0], window[1]]`` by shifting and scaling.
+        The default value is [-1, 1].
+    window : (2,) array_like, optional
+        Window, see `domain` for its use. The default value is [-1, 1].
+
+        .. versionadded:: 1.6.0
+    symbol : str, optional
+        Symbol used to represent the independent variable in string
+        representations of the polynomial expression, e.g. for printing.
+        The symbol must be a valid Python identifier. Default value is 'x'.
+
+        .. versionadded:: 1.24
+
+    """
+    # Virtual Functions
+    _add = staticmethod(chebadd)
+    _sub = staticmethod(chebsub)
+    _mul = staticmethod(chebmul)
+    _div = staticmethod(chebdiv)
+    _pow = staticmethod(chebpow)
+    _val = staticmethod(chebval)
+    _int = staticmethod(chebint)
+    _der = staticmethod(chebder)
+    _fit = staticmethod(chebfit)
+    _line = staticmethod(chebline)
+    _roots = staticmethod(chebroots)
+    _fromroots = staticmethod(chebfromroots)
+
+    @classmethod
+    def interpolate(cls, func, deg, domain=None, args=()):
+        """Interpolate a function at the Chebyshev points of the first kind.
+
+        Returns the series that interpolates `func` at the Chebyshev points of
+        the first kind scaled and shifted to the `domain`. The resulting series
+        tends to a minmax approximation of `func` when the function is
+        continuous in the domain.
+
+        .. versionadded:: 1.14.0
+
+        Parameters
+        ----------
+        func : function
+            The function to be interpolated. It must be a function of a single
+            variable of the form ``f(x, a, b, c...)``, where ``a, b, c...`` are
+            extra arguments passed in the `args` parameter.
+        deg : int
+            Degree of the interpolating polynomial.
+        domain : {None, [beg, end]}, optional
+            Domain over which `func` is interpolated. The default is None, in
+            which case the domain is [-1, 1].
+        args : tuple, optional
+            Extra arguments to be used in the function call. Default is no
+            extra arguments.
+
+        Returns
+        -------
+        polynomial : Chebyshev instance
+            Interpolating Chebyshev instance.
+
+        Notes
+        -----
+        See `numpy.polynomial.chebfromfunction` for more details.
+
+        """
+        if domain is None:
+            domain = cls.domain
+        xfunc = lambda x: func(pu.mapdomain(x, cls.window, domain), *args)
+        coef = chebinterpolate(xfunc, deg)
+        return cls(coef, domain=domain)
+
+    # Virtual properties
+    domain = np.array(chebdomain)
+    window = np.array(chebdomain)
+    basis_name = 'T'
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/chebyshev.pyi b/.venv/lib/python3.12/site-packages/numpy/polynomial/chebyshev.pyi
new file mode 100644
index 00000000..e8113dba
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/chebyshev.pyi
@@ -0,0 +1,51 @@
+from typing import Any
+
+from numpy import ndarray, dtype, int_
+from numpy.polynomial._polybase import ABCPolyBase
+from numpy.polynomial.polyutils import trimcoef
+
+__all__: list[str]
+
+chebtrim = trimcoef
+
+def poly2cheb(pol): ...
+def cheb2poly(c): ...
+
+chebdomain: ndarray[Any, dtype[int_]]
+chebzero: ndarray[Any, dtype[int_]]
+chebone: ndarray[Any, dtype[int_]]
+chebx: ndarray[Any, dtype[int_]]
+
+def chebline(off, scl): ...
+def chebfromroots(roots): ...
+def chebadd(c1, c2): ...
+def chebsub(c1, c2): ...
+def chebmulx(c): ...
+def chebmul(c1, c2): ...
+def chebdiv(c1, c2): ...
+def chebpow(c, pow, maxpower=...): ...
+def chebder(c, m=..., scl=..., axis=...): ...
+def chebint(c, m=..., k = ..., lbnd=..., scl=..., axis=...): ...
+def chebval(x, c, tensor=...): ...
+def chebval2d(x, y, c): ...
+def chebgrid2d(x, y, c): ...
+def chebval3d(x, y, z, c): ...
+def chebgrid3d(x, y, z, c): ...
+def chebvander(x, deg): ...
+def chebvander2d(x, y, deg): ...
+def chebvander3d(x, y, z, deg): ...
+def chebfit(x, y, deg, rcond=..., full=..., w=...): ...
+def chebcompanion(c): ...
+def chebroots(c): ...
+def chebinterpolate(func, deg, args = ...): ...
+def chebgauss(deg): ...
+def chebweight(x): ...
+def chebpts1(npts): ...
+def chebpts2(npts): ...
+
+class Chebyshev(ABCPolyBase):
+    @classmethod
+    def interpolate(cls, func, deg, domain=..., args = ...): ...
+    domain: Any
+    window: Any
+    basis_name: Any
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite.py
new file mode 100644
index 00000000..210df25f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite.py
@@ -0,0 +1,1703 @@
+"""
+==============================================================
+Hermite Series, "Physicists" (:mod:`numpy.polynomial.hermite`)
+==============================================================
+
+This module provides a number of objects (mostly functions) useful for
+dealing with Hermite series, including a `Hermite` class that
+encapsulates the usual arithmetic operations.  (General information
+on how this module represents and works with such polynomials is in the
+docstring for its "parent" sub-package, `numpy.polynomial`).
+
+Classes
+-------
+.. autosummary::
+   :toctree: generated/
+
+   Hermite
+
+Constants
+---------
+.. autosummary::
+   :toctree: generated/
+
+   hermdomain
+   hermzero
+   hermone
+   hermx
+
+Arithmetic
+----------
+.. autosummary::
+   :toctree: generated/
+
+   hermadd
+   hermsub
+   hermmulx
+   hermmul
+   hermdiv
+   hermpow
+   hermval
+   hermval2d
+   hermval3d
+   hermgrid2d
+   hermgrid3d
+
+Calculus
+--------
+.. autosummary::
+   :toctree: generated/
+
+   hermder
+   hermint
+
+Misc Functions
+--------------
+.. autosummary::
+   :toctree: generated/
+
+   hermfromroots
+   hermroots
+   hermvander
+   hermvander2d
+   hermvander3d
+   hermgauss
+   hermweight
+   hermcompanion
+   hermfit
+   hermtrim
+   hermline
+   herm2poly
+   poly2herm
+
+See also
+--------
+`numpy.polynomial`
+
+"""
+import numpy as np
+import numpy.linalg as la
+from numpy.core.multiarray import normalize_axis_index
+
+from . import polyutils as pu
+from ._polybase import ABCPolyBase
+
+__all__ = [
+    'hermzero', 'hermone', 'hermx', 'hermdomain', 'hermline', 'hermadd',
+    'hermsub', 'hermmulx', 'hermmul', 'hermdiv', 'hermpow', 'hermval',
+    'hermder', 'hermint', 'herm2poly', 'poly2herm', 'hermfromroots',
+    'hermvander', 'hermfit', 'hermtrim', 'hermroots', 'Hermite',
+    'hermval2d', 'hermval3d', 'hermgrid2d', 'hermgrid3d', 'hermvander2d',
+    'hermvander3d', 'hermcompanion', 'hermgauss', 'hermweight']
+
+hermtrim = pu.trimcoef
+
+
+def poly2herm(pol):
+    """
+    poly2herm(pol)
+
+    Convert a polynomial to a Hermite series.
+
+    Convert an array representing the coefficients of a polynomial (relative
+    to the "standard" basis) ordered from lowest degree to highest, to an
+    array of the coefficients of the equivalent Hermite series, ordered
+    from lowest to highest degree.
+
+    Parameters
+    ----------
+    pol : array_like
+        1-D array containing the polynomial coefficients
+
+    Returns
+    -------
+    c : ndarray
+        1-D array containing the coefficients of the equivalent Hermite
+        series.
+
+    See Also
+    --------
+    herm2poly
+
+    Notes
+    -----
+    The easy way to do conversions between polynomial basis sets
+    is to use the convert method of a class instance.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import poly2herm
+    >>> poly2herm(np.arange(4))
+    array([1.   ,  2.75 ,  0.5  ,  0.375])
+
+    """
+    [pol] = pu.as_series([pol])
+    deg = len(pol) - 1
+    res = 0
+    for i in range(deg, -1, -1):
+        res = hermadd(hermmulx(res), pol[i])
+    return res
+
+
+def herm2poly(c):
+    """
+    Convert a Hermite series to a polynomial.
+
+    Convert an array representing the coefficients of a Hermite series,
+    ordered from lowest degree to highest, to an array of the coefficients
+    of the equivalent polynomial (relative to the "standard" basis) ordered
+    from lowest to highest degree.
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array containing the Hermite series coefficients, ordered
+        from lowest order term to highest.
+
+    Returns
+    -------
+    pol : ndarray
+        1-D array containing the coefficients of the equivalent polynomial
+        (relative to the "standard" basis) ordered from lowest order term
+        to highest.
+
+    See Also
+    --------
+    poly2herm
+
+    Notes
+    -----
+    The easy way to do conversions between polynomial basis sets
+    is to use the convert method of a class instance.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import herm2poly
+    >>> herm2poly([ 1.   ,  2.75 ,  0.5  ,  0.375])
+    array([0., 1., 2., 3.])
+
+    """
+    from .polynomial import polyadd, polysub, polymulx
+
+    [c] = pu.as_series([c])
+    n = len(c)
+    if n == 1:
+        return c
+    if n == 2:
+        c[1] *= 2
+        return c
+    else:
+        c0 = c[-2]
+        c1 = c[-1]
+        # i is the current degree of c1
+        for i in range(n - 1, 1, -1):
+            tmp = c0
+            c0 = polysub(c[i - 2], c1*(2*(i - 1)))
+            c1 = polyadd(tmp, polymulx(c1)*2)
+        return polyadd(c0, polymulx(c1)*2)
+
+#
+# These are constant arrays are of integer type so as to be compatible
+# with the widest range of other types, such as Decimal.
+#
+
+# Hermite
+hermdomain = np.array([-1, 1])
+
+# Hermite coefficients representing zero.
+hermzero = np.array([0])
+
+# Hermite coefficients representing one.
+hermone = np.array([1])
+
+# Hermite coefficients representing the identity x.
+hermx = np.array([0, 1/2])
+
+
+def hermline(off, scl):
+    """
+    Hermite series whose graph is a straight line.
+
+
+
+    Parameters
+    ----------
+    off, scl : scalars
+        The specified line is given by ``off + scl*x``.
+
+    Returns
+    -------
+    y : ndarray
+        This module's representation of the Hermite series for
+        ``off + scl*x``.
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyline
+    numpy.polynomial.chebyshev.chebline
+    numpy.polynomial.legendre.legline
+    numpy.polynomial.laguerre.lagline
+    numpy.polynomial.hermite_e.hermeline
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermline, hermval
+    >>> hermval(0,hermline(3, 2))
+    3.0
+    >>> hermval(1,hermline(3, 2))
+    5.0
+
+    """
+    if scl != 0:
+        return np.array([off, scl/2])
+    else:
+        return np.array([off])
+
+
+def hermfromroots(roots):
+    """
+    Generate a Hermite series with given roots.
+
+    The function returns the coefficients of the polynomial
+
+    .. math:: p(x) = (x - r_0) * (x - r_1) * ... * (x - r_n),
+
+    in Hermite form, where the `r_n` are the roots specified in `roots`.
+    If a zero has multiplicity n, then it must appear in `roots` n times.
+    For instance, if 2 is a root of multiplicity three and 3 is a root of
+    multiplicity 2, then `roots` looks something like [2, 2, 2, 3, 3]. The
+    roots can appear in any order.
+
+    If the returned coefficients are `c`, then
+
+    .. math:: p(x) = c_0 + c_1 * H_1(x) + ... +  c_n * H_n(x)
+
+    The coefficient of the last term is not generally 1 for monic
+    polynomials in Hermite form.
+
+    Parameters
+    ----------
+    roots : array_like
+        Sequence containing the roots.
+
+    Returns
+    -------
+    out : ndarray
+        1-D array of coefficients.  If all roots are real then `out` is a
+        real array, if some of the roots are complex, then `out` is complex
+        even if all the coefficients in the result are real (see Examples
+        below).
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyfromroots
+    numpy.polynomial.legendre.legfromroots
+    numpy.polynomial.laguerre.lagfromroots
+    numpy.polynomial.chebyshev.chebfromroots
+    numpy.polynomial.hermite_e.hermefromroots
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermfromroots, hermval
+    >>> coef = hermfromroots((-1, 0, 1))
+    >>> hermval((-1, 0, 1), coef)
+    array([0.,  0.,  0.])
+    >>> coef = hermfromroots((-1j, 1j))
+    >>> hermval((-1j, 1j), coef)
+    array([0.+0.j, 0.+0.j])
+
+    """
+    return pu._fromroots(hermline, hermmul, roots)
+
+
+def hermadd(c1, c2):
+    """
+    Add one Hermite series to another.
+
+    Returns the sum of two Hermite series `c1` + `c2`.  The arguments
+    are sequences of coefficients ordered from lowest order term to
+    highest, i.e., [1,2,3] represents the series ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Hermite series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Array representing the Hermite series of their sum.
+
+    See Also
+    --------
+    hermsub, hermmulx, hermmul, hermdiv, hermpow
+
+    Notes
+    -----
+    Unlike multiplication, division, etc., the sum of two Hermite series
+    is a Hermite series (without having to "reproject" the result onto
+    the basis set) so addition, just like that of "standard" polynomials,
+    is simply "component-wise."
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermadd
+    >>> hermadd([1, 2, 3], [1, 2, 3, 4])
+    array([2., 4., 6., 4.])
+
+    """
+    return pu._add(c1, c2)
+
+
+def hermsub(c1, c2):
+    """
+    Subtract one Hermite series from another.
+
+    Returns the difference of two Hermite series `c1` - `c2`.  The
+    sequences of coefficients are from lowest order term to highest, i.e.,
+    [1,2,3] represents the series ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Hermite series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Of Hermite series coefficients representing their difference.
+
+    See Also
+    --------
+    hermadd, hermmulx, hermmul, hermdiv, hermpow
+
+    Notes
+    -----
+    Unlike multiplication, division, etc., the difference of two Hermite
+    series is a Hermite series (without having to "reproject" the result
+    onto the basis set) so subtraction, just like that of "standard"
+    polynomials, is simply "component-wise."
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermsub
+    >>> hermsub([1, 2, 3, 4], [1, 2, 3])
+    array([0.,  0.,  0.,  4.])
+
+    """
+    return pu._sub(c1, c2)
+
+
+def hermmulx(c):
+    """Multiply a Hermite series by x.
+
+    Multiply the Hermite series `c` by x, where x is the independent
+    variable.
+
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Hermite series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Array representing the result of the multiplication.
+
+    See Also
+    --------
+    hermadd, hermsub, hermmul, hermdiv, hermpow
+
+    Notes
+    -----
+    The multiplication uses the recursion relationship for Hermite
+    polynomials in the form
+
+    .. math::
+
+        xP_i(x) = (P_{i + 1}(x)/2 + i*P_{i - 1}(x))
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermmulx
+    >>> hermmulx([1, 2, 3])
+    array([2. , 6.5, 1. , 1.5])
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    # The zero series needs special treatment
+    if len(c) == 1 and c[0] == 0:
+        return c
+
+    prd = np.empty(len(c) + 1, dtype=c.dtype)
+    prd[0] = c[0]*0
+    prd[1] = c[0]/2
+    for i in range(1, len(c)):
+        prd[i + 1] = c[i]/2
+        prd[i - 1] += c[i]*i
+    return prd
+
+
+def hermmul(c1, c2):
+    """
+    Multiply one Hermite series by another.
+
+    Returns the product of two Hermite series `c1` * `c2`.  The arguments
+    are sequences of coefficients, from lowest order "term" to highest,
+    e.g., [1,2,3] represents the series ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Hermite series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Of Hermite series coefficients representing their product.
+
+    See Also
+    --------
+    hermadd, hermsub, hermmulx, hermdiv, hermpow
+
+    Notes
+    -----
+    In general, the (polynomial) product of two C-series results in terms
+    that are not in the Hermite polynomial basis set.  Thus, to express
+    the product as a Hermite series, it is necessary to "reproject" the
+    product onto said basis set, which may produce "unintuitive" (but
+    correct) results; see Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermmul
+    >>> hermmul([1, 2, 3], [0, 1, 2])
+    array([52.,  29.,  52.,   7.,   6.])
+
+    """
+    # s1, s2 are trimmed copies
+    [c1, c2] = pu.as_series([c1, c2])
+
+    if len(c1) > len(c2):
+        c = c2
+        xs = c1
+    else:
+        c = c1
+        xs = c2
+
+    if len(c) == 1:
+        c0 = c[0]*xs
+        c1 = 0
+    elif len(c) == 2:
+        c0 = c[0]*xs
+        c1 = c[1]*xs
+    else:
+        nd = len(c)
+        c0 = c[-2]*xs
+        c1 = c[-1]*xs
+        for i in range(3, len(c) + 1):
+            tmp = c0
+            nd = nd - 1
+            c0 = hermsub(c[-i]*xs, c1*(2*(nd - 1)))
+            c1 = hermadd(tmp, hermmulx(c1)*2)
+    return hermadd(c0, hermmulx(c1)*2)
+
+
+def hermdiv(c1, c2):
+    """
+    Divide one Hermite series by another.
+
+    Returns the quotient-with-remainder of two Hermite series
+    `c1` / `c2`.  The arguments are sequences of coefficients from lowest
+    order "term" to highest, e.g., [1,2,3] represents the series
+    ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Hermite series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    [quo, rem] : ndarrays
+        Of Hermite series coefficients representing the quotient and
+        remainder.
+
+    See Also
+    --------
+    hermadd, hermsub, hermmulx, hermmul, hermpow
+
+    Notes
+    -----
+    In general, the (polynomial) division of one Hermite series by another
+    results in quotient and remainder terms that are not in the Hermite
+    polynomial basis set.  Thus, to express these results as a Hermite
+    series, it is necessary to "reproject" the results onto the Hermite
+    basis set, which may produce "unintuitive" (but correct) results; see
+    Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermdiv
+    >>> hermdiv([ 52.,  29.,  52.,   7.,   6.], [0, 1, 2])
+    (array([1., 2., 3.]), array([0.]))
+    >>> hermdiv([ 54.,  31.,  52.,   7.,   6.], [0, 1, 2])
+    (array([1., 2., 3.]), array([2., 2.]))
+    >>> hermdiv([ 53.,  30.,  52.,   7.,   6.], [0, 1, 2])
+    (array([1., 2., 3.]), array([1., 1.]))
+
+    """
+    return pu._div(hermmul, c1, c2)
+
+
+def hermpow(c, pow, maxpower=16):
+    """Raise a Hermite series to a power.
+
+    Returns the Hermite series `c` raised to the power `pow`. The
+    argument `c` is a sequence of coefficients ordered from low to high.
+    i.e., [1,2,3] is the series  ``P_0 + 2*P_1 + 3*P_2.``
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Hermite series coefficients ordered from low to
+        high.
+    pow : integer
+        Power to which the series will be raised
+    maxpower : integer, optional
+        Maximum power allowed. This is mainly to limit growth of the series
+        to unmanageable size. Default is 16
+
+    Returns
+    -------
+    coef : ndarray
+        Hermite series of power.
+
+    See Also
+    --------
+    hermadd, hermsub, hermmulx, hermmul, hermdiv
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermpow
+    >>> hermpow([1, 2, 3], 2)
+    array([81.,  52.,  82.,  12.,   9.])
+
+    """
+    return pu._pow(hermmul, c, pow, maxpower)
+
+
+def hermder(c, m=1, scl=1, axis=0):
+    """
+    Differentiate a Hermite series.
+
+    Returns the Hermite series coefficients `c` differentiated `m` times
+    along `axis`.  At each iteration the result is multiplied by `scl` (the
+    scaling factor is for use in a linear change of variable). The argument
+    `c` is an array of coefficients from low to high degree along each
+    axis, e.g., [1,2,3] represents the series ``1*H_0 + 2*H_1 + 3*H_2``
+    while [[1,2],[1,2]] represents ``1*H_0(x)*H_0(y) + 1*H_1(x)*H_0(y) +
+    2*H_0(x)*H_1(y) + 2*H_1(x)*H_1(y)`` if axis=0 is ``x`` and axis=1 is
+    ``y``.
+
+    Parameters
+    ----------
+    c : array_like
+        Array of Hermite series coefficients. If `c` is multidimensional the
+        different axis correspond to different variables with the degree in
+        each axis given by the corresponding index.
+    m : int, optional
+        Number of derivatives taken, must be non-negative. (Default: 1)
+    scl : scalar, optional
+        Each differentiation is multiplied by `scl`.  The end result is
+        multiplication by ``scl**m``.  This is for use in a linear change of
+        variable. (Default: 1)
+    axis : int, optional
+        Axis over which the derivative is taken. (Default: 0).
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    der : ndarray
+        Hermite series of the derivative.
+
+    See Also
+    --------
+    hermint
+
+    Notes
+    -----
+    In general, the result of differentiating a Hermite series does not
+    resemble the same operation on a power series. Thus the result of this
+    function may be "unintuitive," albeit correct; see Examples section
+    below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermder
+    >>> hermder([ 1. ,  0.5,  0.5,  0.5])
+    array([1., 2., 3.])
+    >>> hermder([-0.5,  1./2.,  1./8.,  1./12.,  1./16.], m=2)
+    array([1., 2., 3.])
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    cnt = pu._deprecate_as_int(m, "the order of derivation")
+    iaxis = pu._deprecate_as_int(axis, "the axis")
+    if cnt < 0:
+        raise ValueError("The order of derivation must be non-negative")
+    iaxis = normalize_axis_index(iaxis, c.ndim)
+
+    if cnt == 0:
+        return c
+
+    c = np.moveaxis(c, iaxis, 0)
+    n = len(c)
+    if cnt >= n:
+        c = c[:1]*0
+    else:
+        for i in range(cnt):
+            n = n - 1
+            c *= scl
+            der = np.empty((n,) + c.shape[1:], dtype=c.dtype)
+            for j in range(n, 0, -1):
+                der[j - 1] = (2*j)*c[j]
+            c = der
+    c = np.moveaxis(c, 0, iaxis)
+    return c
+
+
+def hermint(c, m=1, k=[], lbnd=0, scl=1, axis=0):
+    """
+    Integrate a Hermite series.
+
+    Returns the Hermite series coefficients `c` integrated `m` times from
+    `lbnd` along `axis`. At each iteration the resulting series is
+    **multiplied** by `scl` and an integration constant, `k`, is added.
+    The scaling factor is for use in a linear change of variable.  ("Buyer
+    beware": note that, depending on what one is doing, one may want `scl`
+    to be the reciprocal of what one might expect; for more information,
+    see the Notes section below.)  The argument `c` is an array of
+    coefficients from low to high degree along each axis, e.g., [1,2,3]
+    represents the series ``H_0 + 2*H_1 + 3*H_2`` while [[1,2],[1,2]]
+    represents ``1*H_0(x)*H_0(y) + 1*H_1(x)*H_0(y) + 2*H_0(x)*H_1(y) +
+    2*H_1(x)*H_1(y)`` if axis=0 is ``x`` and axis=1 is ``y``.
+
+    Parameters
+    ----------
+    c : array_like
+        Array of Hermite series coefficients. If c is multidimensional the
+        different axis correspond to different variables with the degree in
+        each axis given by the corresponding index.
+    m : int, optional
+        Order of integration, must be positive. (Default: 1)
+    k : {[], list, scalar}, optional
+        Integration constant(s).  The value of the first integral at
+        ``lbnd`` is the first value in the list, the value of the second
+        integral at ``lbnd`` is the second value, etc.  If ``k == []`` (the
+        default), all constants are set to zero.  If ``m == 1``, a single
+        scalar can be given instead of a list.
+    lbnd : scalar, optional
+        The lower bound of the integral. (Default: 0)
+    scl : scalar, optional
+        Following each integration the result is *multiplied* by `scl`
+        before the integration constant is added. (Default: 1)
+    axis : int, optional
+        Axis over which the integral is taken. (Default: 0).
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    S : ndarray
+        Hermite series coefficients of the integral.
+
+    Raises
+    ------
+    ValueError
+        If ``m < 0``, ``len(k) > m``, ``np.ndim(lbnd) != 0``, or
+        ``np.ndim(scl) != 0``.
+
+    See Also
+    --------
+    hermder
+
+    Notes
+    -----
+    Note that the result of each integration is *multiplied* by `scl`.
+    Why is this important to note?  Say one is making a linear change of
+    variable :math:`u = ax + b` in an integral relative to `x`.  Then
+    :math:`dx = du/a`, so one will need to set `scl` equal to
+    :math:`1/a` - perhaps not what one would have first thought.
+
+    Also note that, in general, the result of integrating a C-series needs
+    to be "reprojected" onto the C-series basis set.  Thus, typically,
+    the result of this function is "unintuitive," albeit correct; see
+    Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermint
+    >>> hermint([1,2,3]) # integrate once, value 0 at 0.
+    array([1. , 0.5, 0.5, 0.5])
+    >>> hermint([1,2,3], m=2) # integrate twice, value & deriv 0 at 0
+    array([-0.5       ,  0.5       ,  0.125     ,  0.08333333,  0.0625    ]) # may vary
+    >>> hermint([1,2,3], k=1) # integrate once, value 1 at 0.
+    array([2. , 0.5, 0.5, 0.5])
+    >>> hermint([1,2,3], lbnd=-1) # integrate once, value 0 at -1
+    array([-2. ,  0.5,  0.5,  0.5])
+    >>> hermint([1,2,3], m=2, k=[1,2], lbnd=-1)
+    array([ 1.66666667, -0.5       ,  0.125     ,  0.08333333,  0.0625    ]) # may vary
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    if not np.iterable(k):
+        k = [k]
+    cnt = pu._deprecate_as_int(m, "the order of integration")
+    iaxis = pu._deprecate_as_int(axis, "the axis")
+    if cnt < 0:
+        raise ValueError("The order of integration must be non-negative")
+    if len(k) > cnt:
+        raise ValueError("Too many integration constants")
+    if np.ndim(lbnd) != 0:
+        raise ValueError("lbnd must be a scalar.")
+    if np.ndim(scl) != 0:
+        raise ValueError("scl must be a scalar.")
+    iaxis = normalize_axis_index(iaxis, c.ndim)
+
+    if cnt == 0:
+        return c
+
+    c = np.moveaxis(c, iaxis, 0)
+    k = list(k) + [0]*(cnt - len(k))
+    for i in range(cnt):
+        n = len(c)
+        c *= scl
+        if n == 1 and np.all(c[0] == 0):
+            c[0] += k[i]
+        else:
+            tmp = np.empty((n + 1,) + c.shape[1:], dtype=c.dtype)
+            tmp[0] = c[0]*0
+            tmp[1] = c[0]/2
+            for j in range(1, n):
+                tmp[j + 1] = c[j]/(2*(j + 1))
+            tmp[0] += k[i] - hermval(lbnd, tmp)
+            c = tmp
+    c = np.moveaxis(c, 0, iaxis)
+    return c
+
+
+def hermval(x, c, tensor=True):
+    """
+    Evaluate an Hermite series at points x.
+
+    If `c` is of length `n + 1`, this function returns the value:
+
+    .. math:: p(x) = c_0 * H_0(x) + c_1 * H_1(x) + ... + c_n * H_n(x)
+
+    The parameter `x` is converted to an array only if it is a tuple or a
+    list, otherwise it is treated as a scalar. In either case, either `x`
+    or its elements must support multiplication and addition both with
+    themselves and with the elements of `c`.
+
+    If `c` is a 1-D array, then `p(x)` will have the same shape as `x`.  If
+    `c` is multidimensional, then the shape of the result depends on the
+    value of `tensor`. If `tensor` is true the shape will be c.shape[1:] +
+    x.shape. If `tensor` is false the shape will be c.shape[1:]. Note that
+    scalars have shape (,).
+
+    Trailing zeros in the coefficients will be used in the evaluation, so
+    they should be avoided if efficiency is a concern.
+
+    Parameters
+    ----------
+    x : array_like, compatible object
+        If `x` is a list or tuple, it is converted to an ndarray, otherwise
+        it is left unchanged and treated as a scalar. In either case, `x`
+        or its elements must support addition and multiplication with
+        themselves and with the elements of `c`.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree n are contained in c[n]. If `c` is multidimensional the
+        remaining indices enumerate multiple polynomials. In the two
+        dimensional case the coefficients may be thought of as stored in
+        the columns of `c`.
+    tensor : boolean, optional
+        If True, the shape of the coefficient array is extended with ones
+        on the right, one for each dimension of `x`. Scalars have dimension 0
+        for this action. The result is that every column of coefficients in
+        `c` is evaluated for every element of `x`. If False, `x` is broadcast
+        over the columns of `c` for the evaluation.  This keyword is useful
+        when `c` is multidimensional. The default value is True.
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    values : ndarray, algebra_like
+        The shape of the return value is described above.
+
+    See Also
+    --------
+    hermval2d, hermgrid2d, hermval3d, hermgrid3d
+
+    Notes
+    -----
+    The evaluation uses Clenshaw recursion, aka synthetic division.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermval
+    >>> coef = [1,2,3]
+    >>> hermval(1, coef)
+    11.0
+    >>> hermval([[1,2],[3,4]], coef)
+    array([[ 11.,   51.],
+           [115.,  203.]])
+
+    """
+    c = np.array(c, ndmin=1, copy=False)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    if isinstance(x, (tuple, list)):
+        x = np.asarray(x)
+    if isinstance(x, np.ndarray) and tensor:
+        c = c.reshape(c.shape + (1,)*x.ndim)
+
+    x2 = x*2
+    if len(c) == 1:
+        c0 = c[0]
+        c1 = 0
+    elif len(c) == 2:
+        c0 = c[0]
+        c1 = c[1]
+    else:
+        nd = len(c)
+        c0 = c[-2]
+        c1 = c[-1]
+        for i in range(3, len(c) + 1):
+            tmp = c0
+            nd = nd - 1
+            c0 = c[-i] - c1*(2*(nd - 1))
+            c1 = tmp + c1*x2
+    return c0 + c1*x2
+
+
+def hermval2d(x, y, c):
+    """
+    Evaluate a 2-D Hermite series at points (x, y).
+
+    This function returns the values:
+
+    .. math:: p(x,y) = \\sum_{i,j} c_{i,j} * H_i(x) * H_j(y)
+
+    The parameters `x` and `y` are converted to arrays only if they are
+    tuples or a lists, otherwise they are treated as a scalars and they
+    must have the same shape after conversion. In either case, either `x`
+    and `y` or their elements must support multiplication and addition both
+    with themselves and with the elements of `c`.
+
+    If `c` is a 1-D array a one is implicitly appended to its shape to make
+    it 2-D. The shape of the result will be c.shape[2:] + x.shape.
+
+    Parameters
+    ----------
+    x, y : array_like, compatible objects
+        The two dimensional series is evaluated at the points `(x, y)`,
+        where `x` and `y` must have the same shape. If `x` or `y` is a list
+        or tuple, it is first converted to an ndarray, otherwise it is left
+        unchanged and if it isn't an ndarray it is treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term
+        of multi-degree i,j is contained in ``c[i,j]``. If `c` has
+        dimension greater than two the remaining indices enumerate multiple
+        sets of coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points formed with
+        pairs of corresponding values from `x` and `y`.
+
+    See Also
+    --------
+    hermval, hermgrid2d, hermval3d, hermgrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._valnd(hermval, c, x, y)
+
+
+def hermgrid2d(x, y, c):
+    """
+    Evaluate a 2-D Hermite series on the Cartesian product of x and y.
+
+    This function returns the values:
+
+    .. math:: p(a,b) = \\sum_{i,j} c_{i,j} * H_i(a) * H_j(b)
+
+    where the points `(a, b)` consist of all pairs formed by taking
+    `a` from `x` and `b` from `y`. The resulting points form a grid with
+    `x` in the first dimension and `y` in the second.
+
+    The parameters `x` and `y` are converted to arrays only if they are
+    tuples or a lists, otherwise they are treated as a scalars. In either
+    case, either `x` and `y` or their elements must support multiplication
+    and addition both with themselves and with the elements of `c`.
+
+    If `c` has fewer than two dimensions, ones are implicitly appended to
+    its shape to make it 2-D. The shape of the result will be c.shape[2:] +
+    x.shape.
+
+    Parameters
+    ----------
+    x, y : array_like, compatible objects
+        The two dimensional series is evaluated at the points in the
+        Cartesian product of `x` and `y`.  If `x` or `y` is a list or
+        tuple, it is first converted to an ndarray, otherwise it is left
+        unchanged and, if it isn't an ndarray, it is treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree i,j are contained in ``c[i,j]``. If `c` has dimension
+        greater than two the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points in the Cartesian
+        product of `x` and `y`.
+
+    See Also
+    --------
+    hermval, hermval2d, hermval3d, hermgrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._gridnd(hermval, c, x, y)
+
+
+def hermval3d(x, y, z, c):
+    """
+    Evaluate a 3-D Hermite series at points (x, y, z).
+
+    This function returns the values:
+
+    .. math:: p(x,y,z) = \\sum_{i,j,k} c_{i,j,k} * H_i(x) * H_j(y) * H_k(z)
+
+    The parameters `x`, `y`, and `z` are converted to arrays only if
+    they are tuples or a lists, otherwise they are treated as a scalars and
+    they must have the same shape after conversion. In either case, either
+    `x`, `y`, and `z` or their elements must support multiplication and
+    addition both with themselves and with the elements of `c`.
+
+    If `c` has fewer than 3 dimensions, ones are implicitly appended to its
+    shape to make it 3-D. The shape of the result will be c.shape[3:] +
+    x.shape.
+
+    Parameters
+    ----------
+    x, y, z : array_like, compatible object
+        The three dimensional series is evaluated at the points
+        `(x, y, z)`, where `x`, `y`, and `z` must have the same shape.  If
+        any of `x`, `y`, or `z` is a list or tuple, it is first converted
+        to an ndarray, otherwise it is left unchanged and if it isn't an
+        ndarray it is  treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term of
+        multi-degree i,j,k is contained in ``c[i,j,k]``. If `c` has dimension
+        greater than 3 the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the multidimensional polynomial on points formed with
+        triples of corresponding values from `x`, `y`, and `z`.
+
+    See Also
+    --------
+    hermval, hermval2d, hermgrid2d, hermgrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._valnd(hermval, c, x, y, z)
+
+
+def hermgrid3d(x, y, z, c):
+    """
+    Evaluate a 3-D Hermite series on the Cartesian product of x, y, and z.
+
+    This function returns the values:
+
+    .. math:: p(a,b,c) = \\sum_{i,j,k} c_{i,j,k} * H_i(a) * H_j(b) * H_k(c)
+
+    where the points `(a, b, c)` consist of all triples formed by taking
+    `a` from `x`, `b` from `y`, and `c` from `z`. The resulting points form
+    a grid with `x` in the first dimension, `y` in the second, and `z` in
+    the third.
+
+    The parameters `x`, `y`, and `z` are converted to arrays only if they
+    are tuples or a lists, otherwise they are treated as a scalars. In
+    either case, either `x`, `y`, and `z` or their elements must support
+    multiplication and addition both with themselves and with the elements
+    of `c`.
+
+    If `c` has fewer than three dimensions, ones are implicitly appended to
+    its shape to make it 3-D. The shape of the result will be c.shape[3:] +
+    x.shape + y.shape + z.shape.
+
+    Parameters
+    ----------
+    x, y, z : array_like, compatible objects
+        The three dimensional series is evaluated at the points in the
+        Cartesian product of `x`, `y`, and `z`.  If `x`,`y`, or `z` is a
+        list or tuple, it is first converted to an ndarray, otherwise it is
+        left unchanged and, if it isn't an ndarray, it is treated as a
+        scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree i,j are contained in ``c[i,j]``. If `c` has dimension
+        greater than two the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points in the Cartesian
+        product of `x` and `y`.
+
+    See Also
+    --------
+    hermval, hermval2d, hermgrid2d, hermval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._gridnd(hermval, c, x, y, z)
+
+
+def hermvander(x, deg):
+    """Pseudo-Vandermonde matrix of given degree.
+
+    Returns the pseudo-Vandermonde matrix of degree `deg` and sample points
+    `x`. The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., i] = H_i(x),
+
+    where `0 <= i <= deg`. The leading indices of `V` index the elements of
+    `x` and the last index is the degree of the Hermite polynomial.
+
+    If `c` is a 1-D array of coefficients of length `n + 1` and `V` is the
+    array ``V = hermvander(x, n)``, then ``np.dot(V, c)`` and
+    ``hermval(x, c)`` are the same up to roundoff. This equivalence is
+    useful both for least squares fitting and for the evaluation of a large
+    number of Hermite series of the same degree and sample points.
+
+    Parameters
+    ----------
+    x : array_like
+        Array of points. The dtype is converted to float64 or complex128
+        depending on whether any of the elements are complex. If `x` is
+        scalar it is converted to a 1-D array.
+    deg : int
+        Degree of the resulting matrix.
+
+    Returns
+    -------
+    vander : ndarray
+        The pseudo-Vandermonde matrix. The shape of the returned matrix is
+        ``x.shape + (deg + 1,)``, where The last index is the degree of the
+        corresponding Hermite polynomial.  The dtype will be the same as
+        the converted `x`.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermvander
+    >>> x = np.array([-1, 0, 1])
+    >>> hermvander(x, 3)
+    array([[ 1., -2.,  2.,  4.],
+           [ 1.,  0., -2., -0.],
+           [ 1.,  2.,  2., -4.]])
+
+    """
+    ideg = pu._deprecate_as_int(deg, "deg")
+    if ideg < 0:
+        raise ValueError("deg must be non-negative")
+
+    x = np.array(x, copy=False, ndmin=1) + 0.0
+    dims = (ideg + 1,) + x.shape
+    dtyp = x.dtype
+    v = np.empty(dims, dtype=dtyp)
+    v[0] = x*0 + 1
+    if ideg > 0:
+        x2 = x*2
+        v[1] = x2
+        for i in range(2, ideg + 1):
+            v[i] = (v[i-1]*x2 - v[i-2]*(2*(i - 1)))
+    return np.moveaxis(v, 0, -1)
+
+
+def hermvander2d(x, y, deg):
+    """Pseudo-Vandermonde matrix of given degrees.
+
+    Returns the pseudo-Vandermonde matrix of degrees `deg` and sample
+    points `(x, y)`. The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., (deg[1] + 1)*i + j] = H_i(x) * H_j(y),
+
+    where `0 <= i <= deg[0]` and `0 <= j <= deg[1]`. The leading indices of
+    `V` index the points `(x, y)` and the last index encodes the degrees of
+    the Hermite polynomials.
+
+    If ``V = hermvander2d(x, y, [xdeg, ydeg])``, then the columns of `V`
+    correspond to the elements of a 2-D coefficient array `c` of shape
+    (xdeg + 1, ydeg + 1) in the order
+
+    .. math:: c_{00}, c_{01}, c_{02} ... , c_{10}, c_{11}, c_{12} ...
+
+    and ``np.dot(V, c.flat)`` and ``hermval2d(x, y, c)`` will be the same
+    up to roundoff. This equivalence is useful both for least squares
+    fitting and for the evaluation of a large number of 2-D Hermite
+    series of the same degrees and sample points.
+
+    Parameters
+    ----------
+    x, y : array_like
+        Arrays of point coordinates, all of the same shape. The dtypes
+        will be converted to either float64 or complex128 depending on
+        whether any of the elements are complex. Scalars are converted to 1-D
+        arrays.
+    deg : list of ints
+        List of maximum degrees of the form [x_deg, y_deg].
+
+    Returns
+    -------
+    vander2d : ndarray
+        The shape of the returned matrix is ``x.shape + (order,)``, where
+        :math:`order = (deg[0]+1)*(deg[1]+1)`.  The dtype will be the same
+        as the converted `x` and `y`.
+
+    See Also
+    --------
+    hermvander, hermvander3d, hermval2d, hermval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._vander_nd_flat((hermvander, hermvander), (x, y), deg)
+
+
+def hermvander3d(x, y, z, deg):
+    """Pseudo-Vandermonde matrix of given degrees.
+
+    Returns the pseudo-Vandermonde matrix of degrees `deg` and sample
+    points `(x, y, z)`. If `l, m, n` are the given degrees in `x, y, z`,
+    then The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., (m+1)(n+1)i + (n+1)j + k] = H_i(x)*H_j(y)*H_k(z),
+
+    where `0 <= i <= l`, `0 <= j <= m`, and `0 <= j <= n`.  The leading
+    indices of `V` index the points `(x, y, z)` and the last index encodes
+    the degrees of the Hermite polynomials.
+
+    If ``V = hermvander3d(x, y, z, [xdeg, ydeg, zdeg])``, then the columns
+    of `V` correspond to the elements of a 3-D coefficient array `c` of
+    shape (xdeg + 1, ydeg + 1, zdeg + 1) in the order
+
+    .. math:: c_{000}, c_{001}, c_{002},... , c_{010}, c_{011}, c_{012},...
+
+    and  ``np.dot(V, c.flat)`` and ``hermval3d(x, y, z, c)`` will be the
+    same up to roundoff. This equivalence is useful both for least squares
+    fitting and for the evaluation of a large number of 3-D Hermite
+    series of the same degrees and sample points.
+
+    Parameters
+    ----------
+    x, y, z : array_like
+        Arrays of point coordinates, all of the same shape. The dtypes will
+        be converted to either float64 or complex128 depending on whether
+        any of the elements are complex. Scalars are converted to 1-D
+        arrays.
+    deg : list of ints
+        List of maximum degrees of the form [x_deg, y_deg, z_deg].
+
+    Returns
+    -------
+    vander3d : ndarray
+        The shape of the returned matrix is ``x.shape + (order,)``, where
+        :math:`order = (deg[0]+1)*(deg[1]+1)*(deg[2]+1)`.  The dtype will
+        be the same as the converted `x`, `y`, and `z`.
+
+    See Also
+    --------
+    hermvander, hermvander3d, hermval2d, hermval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._vander_nd_flat((hermvander, hermvander, hermvander), (x, y, z), deg)
+
+
+def hermfit(x, y, deg, rcond=None, full=False, w=None):
+    """
+    Least squares fit of Hermite series to data.
+
+    Return the coefficients of a Hermite series of degree `deg` that is the
+    least squares fit to the data values `y` given at points `x`. If `y` is
+    1-D the returned coefficients will also be 1-D. If `y` is 2-D multiple
+    fits are done, one for each column of `y`, and the resulting
+    coefficients are stored in the corresponding columns of a 2-D return.
+    The fitted polynomial(s) are in the form
+
+    .. math::  p(x) = c_0 + c_1 * H_1(x) + ... + c_n * H_n(x),
+
+    where `n` is `deg`.
+
+    Parameters
+    ----------
+    x : array_like, shape (M,)
+        x-coordinates of the M sample points ``(x[i], y[i])``.
+    y : array_like, shape (M,) or (M, K)
+        y-coordinates of the sample points. Several data sets of sample
+        points sharing the same x-coordinates can be fitted at once by
+        passing in a 2D-array that contains one dataset per column.
+    deg : int or 1-D array_like
+        Degree(s) of the fitting polynomials. If `deg` is a single integer
+        all terms up to and including the `deg`'th term are included in the
+        fit. For NumPy versions >= 1.11.0 a list of integers specifying the
+        degrees of the terms to include may be used instead.
+    rcond : float, optional
+        Relative condition number of the fit. Singular values smaller than
+        this relative to the largest singular value will be ignored. The
+        default value is len(x)*eps, where eps is the relative precision of
+        the float type, about 2e-16 in most cases.
+    full : bool, optional
+        Switch determining nature of return value. When it is False (the
+        default) just the coefficients are returned, when True diagnostic
+        information from the singular value decomposition is also returned.
+    w : array_like, shape (`M`,), optional
+        Weights. If not None, the weight ``w[i]`` applies to the unsquared
+        residual ``y[i] - y_hat[i]`` at ``x[i]``. Ideally the weights are
+        chosen so that the errors of the products ``w[i]*y[i]`` all have the
+        same variance.  When using inverse-variance weighting, use
+        ``w[i] = 1/sigma(y[i])``.  The default value is None.
+
+    Returns
+    -------
+    coef : ndarray, shape (M,) or (M, K)
+        Hermite coefficients ordered from low to high. If `y` was 2-D,
+        the coefficients for the data in column k  of `y` are in column
+        `k`.
+
+    [residuals, rank, singular_values, rcond] : list
+        These values are only returned if ``full == True``
+
+        - residuals -- sum of squared residuals of the least squares fit
+        - rank -- the numerical rank of the scaled Vandermonde matrix
+        - singular_values -- singular values of the scaled Vandermonde matrix
+        - rcond -- value of `rcond`.
+
+        For more details, see `numpy.linalg.lstsq`.
+
+    Warns
+    -----
+    RankWarning
+        The rank of the coefficient matrix in the least-squares fit is
+        deficient. The warning is only raised if ``full == False``.  The
+        warnings can be turned off by
+
+        >>> import warnings
+        >>> warnings.simplefilter('ignore', np.RankWarning)
+
+    See Also
+    --------
+    numpy.polynomial.chebyshev.chebfit
+    numpy.polynomial.legendre.legfit
+    numpy.polynomial.laguerre.lagfit
+    numpy.polynomial.polynomial.polyfit
+    numpy.polynomial.hermite_e.hermefit
+    hermval : Evaluates a Hermite series.
+    hermvander : Vandermonde matrix of Hermite series.
+    hermweight : Hermite weight function
+    numpy.linalg.lstsq : Computes a least-squares fit from the matrix.
+    scipy.interpolate.UnivariateSpline : Computes spline fits.
+
+    Notes
+    -----
+    The solution is the coefficients of the Hermite series `p` that
+    minimizes the sum of the weighted squared errors
+
+    .. math:: E = \\sum_j w_j^2 * |y_j - p(x_j)|^2,
+
+    where the :math:`w_j` are the weights. This problem is solved by
+    setting up the (typically) overdetermined matrix equation
+
+    .. math:: V(x) * c = w * y,
+
+    where `V` is the weighted pseudo Vandermonde matrix of `x`, `c` are the
+    coefficients to be solved for, `w` are the weights, `y` are the
+    observed values.  This equation is then solved using the singular value
+    decomposition of `V`.
+
+    If some of the singular values of `V` are so small that they are
+    neglected, then a `RankWarning` will be issued. This means that the
+    coefficient values may be poorly determined. Using a lower order fit
+    will usually get rid of the warning.  The `rcond` parameter can also be
+    set to a value smaller than its default, but the resulting fit may be
+    spurious and have large contributions from roundoff error.
+
+    Fits using Hermite series are probably most useful when the data can be
+    approximated by ``sqrt(w(x)) * p(x)``, where `w(x)` is the Hermite
+    weight. In that case the weight ``sqrt(w(x[i]))`` should be used
+    together with data values ``y[i]/sqrt(w(x[i]))``. The weight function is
+    available as `hermweight`.
+
+    References
+    ----------
+    .. [1] Wikipedia, "Curve fitting",
+           https://en.wikipedia.org/wiki/Curve_fitting
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermfit, hermval
+    >>> x = np.linspace(-10, 10)
+    >>> err = np.random.randn(len(x))/10
+    >>> y = hermval(x, [1, 2, 3]) + err
+    >>> hermfit(x, y, 2)
+    array([1.0218, 1.9986, 2.9999]) # may vary
+
+    """
+    return pu._fit(hermvander, x, y, deg, rcond, full, w)
+
+
+def hermcompanion(c):
+    """Return the scaled companion matrix of c.
+
+    The basis polynomials are scaled so that the companion matrix is
+    symmetric when `c` is an Hermite basis polynomial. This provides
+    better eigenvalue estimates than the unscaled case and for basis
+    polynomials the eigenvalues are guaranteed to be real if
+    `numpy.linalg.eigvalsh` is used to obtain them.
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Hermite series coefficients ordered from low to high
+        degree.
+
+    Returns
+    -------
+    mat : ndarray
+        Scaled companion matrix of dimensions (deg, deg).
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    if len(c) < 2:
+        raise ValueError('Series must have maximum degree of at least 1.')
+    if len(c) == 2:
+        return np.array([[-.5*c[0]/c[1]]])
+
+    n = len(c) - 1
+    mat = np.zeros((n, n), dtype=c.dtype)
+    scl = np.hstack((1., 1./np.sqrt(2.*np.arange(n - 1, 0, -1))))
+    scl = np.multiply.accumulate(scl)[::-1]
+    top = mat.reshape(-1)[1::n+1]
+    bot = mat.reshape(-1)[n::n+1]
+    top[...] = np.sqrt(.5*np.arange(1, n))
+    bot[...] = top
+    mat[:, -1] -= scl*c[:-1]/(2.0*c[-1])
+    return mat
+
+
+def hermroots(c):
+    """
+    Compute the roots of a Hermite series.
+
+    Return the roots (a.k.a. "zeros") of the polynomial
+
+    .. math:: p(x) = \\sum_i c[i] * H_i(x).
+
+    Parameters
+    ----------
+    c : 1-D array_like
+        1-D array of coefficients.
+
+    Returns
+    -------
+    out : ndarray
+        Array of the roots of the series. If all the roots are real,
+        then `out` is also real, otherwise it is complex.
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyroots
+    numpy.polynomial.legendre.legroots
+    numpy.polynomial.laguerre.lagroots
+    numpy.polynomial.chebyshev.chebroots
+    numpy.polynomial.hermite_e.hermeroots
+
+    Notes
+    -----
+    The root estimates are obtained as the eigenvalues of the companion
+    matrix, Roots far from the origin of the complex plane may have large
+    errors due to the numerical instability of the series for such
+    values. Roots with multiplicity greater than 1 will also show larger
+    errors as the value of the series near such points is relatively
+    insensitive to errors in the roots. Isolated roots near the origin can
+    be improved by a few iterations of Newton's method.
+
+    The Hermite series basis polynomials aren't powers of `x` so the
+    results of this function may seem unintuitive.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite import hermroots, hermfromroots
+    >>> coef = hermfromroots([-1, 0, 1])
+    >>> coef
+    array([0.   ,  0.25 ,  0.   ,  0.125])
+    >>> hermroots(coef)
+    array([-1.00000000e+00, -1.38777878e-17,  1.00000000e+00])
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    if len(c) <= 1:
+        return np.array([], dtype=c.dtype)
+    if len(c) == 2:
+        return np.array([-.5*c[0]/c[1]])
+
+    # rotated companion matrix reduces error
+    m = hermcompanion(c)[::-1,::-1]
+    r = la.eigvals(m)
+    r.sort()
+    return r
+
+
+def _normed_hermite_n(x, n):
+    """
+    Evaluate a normalized Hermite polynomial.
+
+    Compute the value of the normalized Hermite polynomial of degree ``n``
+    at the points ``x``.
+
+
+    Parameters
+    ----------
+    x : ndarray of double.
+        Points at which to evaluate the function
+    n : int
+        Degree of the normalized Hermite function to be evaluated.
+
+    Returns
+    -------
+    values : ndarray
+        The shape of the return value is described above.
+
+    Notes
+    -----
+    .. versionadded:: 1.10.0
+
+    This function is needed for finding the Gauss points and integration
+    weights for high degrees. The values of the standard Hermite functions
+    overflow when n >= 207.
+
+    """
+    if n == 0:
+        return np.full(x.shape, 1/np.sqrt(np.sqrt(np.pi)))
+
+    c0 = 0.
+    c1 = 1./np.sqrt(np.sqrt(np.pi))
+    nd = float(n)
+    for i in range(n - 1):
+        tmp = c0
+        c0 = -c1*np.sqrt((nd - 1.)/nd)
+        c1 = tmp + c1*x*np.sqrt(2./nd)
+        nd = nd - 1.0
+    return c0 + c1*x*np.sqrt(2)
+
+
+def hermgauss(deg):
+    """
+    Gauss-Hermite quadrature.
+
+    Computes the sample points and weights for Gauss-Hermite quadrature.
+    These sample points and weights will correctly integrate polynomials of
+    degree :math:`2*deg - 1` or less over the interval :math:`[-\\inf, \\inf]`
+    with the weight function :math:`f(x) = \\exp(-x^2)`.
+
+    Parameters
+    ----------
+    deg : int
+        Number of sample points and weights. It must be >= 1.
+
+    Returns
+    -------
+    x : ndarray
+        1-D ndarray containing the sample points.
+    y : ndarray
+        1-D ndarray containing the weights.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    The results have only been tested up to degree 100, higher degrees may
+    be problematic. The weights are determined by using the fact that
+
+    .. math:: w_k = c / (H'_n(x_k) * H_{n-1}(x_k))
+
+    where :math:`c` is a constant independent of :math:`k` and :math:`x_k`
+    is the k'th root of :math:`H_n`, and then scaling the results to get
+    the right value when integrating 1.
+
+    """
+    ideg = pu._deprecate_as_int(deg, "deg")
+    if ideg <= 0:
+        raise ValueError("deg must be a positive integer")
+
+    # first approximation of roots. We use the fact that the companion
+    # matrix is symmetric in this case in order to obtain better zeros.
+    c = np.array([0]*deg + [1], dtype=np.float64)
+    m = hermcompanion(c)
+    x = la.eigvalsh(m)
+
+    # improve roots by one application of Newton
+    dy = _normed_hermite_n(x, ideg)
+    df = _normed_hermite_n(x, ideg - 1) * np.sqrt(2*ideg)
+    x -= dy/df
+
+    # compute the weights. We scale the factor to avoid possible numerical
+    # overflow.
+    fm = _normed_hermite_n(x, ideg - 1)
+    fm /= np.abs(fm).max()
+    w = 1/(fm * fm)
+
+    # for Hermite we can also symmetrize
+    w = (w + w[::-1])/2
+    x = (x - x[::-1])/2
+
+    # scale w to get the right value
+    w *= np.sqrt(np.pi) / w.sum()
+
+    return x, w
+
+
+def hermweight(x):
+    """
+    Weight function of the Hermite polynomials.
+
+    The weight function is :math:`\\exp(-x^2)` and the interval of
+    integration is :math:`[-\\inf, \\inf]`. the Hermite polynomials are
+    orthogonal, but not normalized, with respect to this weight function.
+
+    Parameters
+    ----------
+    x : array_like
+       Values at which the weight function will be computed.
+
+    Returns
+    -------
+    w : ndarray
+       The weight function at `x`.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    w = np.exp(-x**2)
+    return w
+
+
+#
+# Hermite series class
+#
+
+class Hermite(ABCPolyBase):
+    """An Hermite series class.
+
+    The Hermite class provides the standard Python numerical methods
+    '+', '-', '*', '//', '%', 'divmod', '**', and '()' as well as the
+    attributes and methods listed in the `ABCPolyBase` documentation.
+
+    Parameters
+    ----------
+    coef : array_like
+        Hermite coefficients in order of increasing degree, i.e,
+        ``(1, 2, 3)`` gives ``1*H_0(x) + 2*H_1(X) + 3*H_2(x)``.
+    domain : (2,) array_like, optional
+        Domain to use. The interval ``[domain[0], domain[1]]`` is mapped
+        to the interval ``[window[0], window[1]]`` by shifting and scaling.
+        The default value is [-1, 1].
+    window : (2,) array_like, optional
+        Window, see `domain` for its use. The default value is [-1, 1].
+
+        .. versionadded:: 1.6.0
+    symbol : str, optional
+        Symbol used to represent the independent variable in string
+        representations of the polynomial expression, e.g. for printing.
+        The symbol must be a valid Python identifier. Default value is 'x'.
+
+        .. versionadded:: 1.24
+
+    """
+    # Virtual Functions
+    _add = staticmethod(hermadd)
+    _sub = staticmethod(hermsub)
+    _mul = staticmethod(hermmul)
+    _div = staticmethod(hermdiv)
+    _pow = staticmethod(hermpow)
+    _val = staticmethod(hermval)
+    _int = staticmethod(hermint)
+    _der = staticmethod(hermder)
+    _fit = staticmethod(hermfit)
+    _line = staticmethod(hermline)
+    _roots = staticmethod(hermroots)
+    _fromroots = staticmethod(hermfromroots)
+
+    # Virtual properties
+    domain = np.array(hermdomain)
+    window = np.array(hermdomain)
+    basis_name = 'H'
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite.pyi b/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite.pyi
new file mode 100644
index 00000000..0d3556d6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite.pyi
@@ -0,0 +1,46 @@
+from typing import Any
+
+from numpy import ndarray, dtype, int_, float_
+from numpy.polynomial._polybase import ABCPolyBase
+from numpy.polynomial.polyutils import trimcoef
+
+__all__: list[str]
+
+hermtrim = trimcoef
+
+def poly2herm(pol): ...
+def herm2poly(c): ...
+
+hermdomain: ndarray[Any, dtype[int_]]
+hermzero: ndarray[Any, dtype[int_]]
+hermone: ndarray[Any, dtype[int_]]
+hermx: ndarray[Any, dtype[float_]]
+
+def hermline(off, scl): ...
+def hermfromroots(roots): ...
+def hermadd(c1, c2): ...
+def hermsub(c1, c2): ...
+def hermmulx(c): ...
+def hermmul(c1, c2): ...
+def hermdiv(c1, c2): ...
+def hermpow(c, pow, maxpower=...): ...
+def hermder(c, m=..., scl=..., axis=...): ...
+def hermint(c, m=..., k = ..., lbnd=..., scl=..., axis=...): ...
+def hermval(x, c, tensor=...): ...
+def hermval2d(x, y, c): ...
+def hermgrid2d(x, y, c): ...
+def hermval3d(x, y, z, c): ...
+def hermgrid3d(x, y, z, c): ...
+def hermvander(x, deg): ...
+def hermvander2d(x, y, deg): ...
+def hermvander3d(x, y, z, deg): ...
+def hermfit(x, y, deg, rcond=..., full=..., w=...): ...
+def hermcompanion(c): ...
+def hermroots(c): ...
+def hermgauss(deg): ...
+def hermweight(x): ...
+
+class Hermite(ABCPolyBase):
+    domain: Any
+    window: Any
+    basis_name: Any
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite_e.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite_e.py
new file mode 100644
index 00000000..bdf29405
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite_e.py
@@ -0,0 +1,1695 @@
+"""
+===================================================================
+HermiteE Series, "Probabilists" (:mod:`numpy.polynomial.hermite_e`)
+===================================================================
+
+This module provides a number of objects (mostly functions) useful for
+dealing with Hermite_e series, including a `HermiteE` class that
+encapsulates the usual arithmetic operations.  (General information
+on how this module represents and works with such polynomials is in the
+docstring for its "parent" sub-package, `numpy.polynomial`).
+
+Classes
+-------
+.. autosummary::
+   :toctree: generated/
+
+   HermiteE
+
+Constants
+---------
+.. autosummary::
+   :toctree: generated/
+
+   hermedomain
+   hermezero
+   hermeone
+   hermex
+
+Arithmetic
+----------
+.. autosummary::
+   :toctree: generated/
+
+   hermeadd
+   hermesub
+   hermemulx
+   hermemul
+   hermediv
+   hermepow
+   hermeval
+   hermeval2d
+   hermeval3d
+   hermegrid2d
+   hermegrid3d
+
+Calculus
+--------
+.. autosummary::
+   :toctree: generated/
+
+   hermeder
+   hermeint
+
+Misc Functions
+--------------
+.. autosummary::
+   :toctree: generated/
+
+   hermefromroots
+   hermeroots
+   hermevander
+   hermevander2d
+   hermevander3d
+   hermegauss
+   hermeweight
+   hermecompanion
+   hermefit
+   hermetrim
+   hermeline
+   herme2poly
+   poly2herme
+
+See also
+--------
+`numpy.polynomial`
+
+"""
+import numpy as np
+import numpy.linalg as la
+from numpy.core.multiarray import normalize_axis_index
+
+from . import polyutils as pu
+from ._polybase import ABCPolyBase
+
+__all__ = [
+    'hermezero', 'hermeone', 'hermex', 'hermedomain', 'hermeline',
+    'hermeadd', 'hermesub', 'hermemulx', 'hermemul', 'hermediv',
+    'hermepow', 'hermeval', 'hermeder', 'hermeint', 'herme2poly',
+    'poly2herme', 'hermefromroots', 'hermevander', 'hermefit', 'hermetrim',
+    'hermeroots', 'HermiteE', 'hermeval2d', 'hermeval3d', 'hermegrid2d',
+    'hermegrid3d', 'hermevander2d', 'hermevander3d', 'hermecompanion',
+    'hermegauss', 'hermeweight']
+
+hermetrim = pu.trimcoef
+
+
+def poly2herme(pol):
+    """
+    poly2herme(pol)
+
+    Convert a polynomial to a Hermite series.
+
+    Convert an array representing the coefficients of a polynomial (relative
+    to the "standard" basis) ordered from lowest degree to highest, to an
+    array of the coefficients of the equivalent Hermite series, ordered
+    from lowest to highest degree.
+
+    Parameters
+    ----------
+    pol : array_like
+        1-D array containing the polynomial coefficients
+
+    Returns
+    -------
+    c : ndarray
+        1-D array containing the coefficients of the equivalent Hermite
+        series.
+
+    See Also
+    --------
+    herme2poly
+
+    Notes
+    -----
+    The easy way to do conversions between polynomial basis sets
+    is to use the convert method of a class instance.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import poly2herme
+    >>> poly2herme(np.arange(4))
+    array([  2.,  10.,   2.,   3.])
+
+    """
+    [pol] = pu.as_series([pol])
+    deg = len(pol) - 1
+    res = 0
+    for i in range(deg, -1, -1):
+        res = hermeadd(hermemulx(res), pol[i])
+    return res
+
+
+def herme2poly(c):
+    """
+    Convert a Hermite series to a polynomial.
+
+    Convert an array representing the coefficients of a Hermite series,
+    ordered from lowest degree to highest, to an array of the coefficients
+    of the equivalent polynomial (relative to the "standard" basis) ordered
+    from lowest to highest degree.
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array containing the Hermite series coefficients, ordered
+        from lowest order term to highest.
+
+    Returns
+    -------
+    pol : ndarray
+        1-D array containing the coefficients of the equivalent polynomial
+        (relative to the "standard" basis) ordered from lowest order term
+        to highest.
+
+    See Also
+    --------
+    poly2herme
+
+    Notes
+    -----
+    The easy way to do conversions between polynomial basis sets
+    is to use the convert method of a class instance.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import herme2poly
+    >>> herme2poly([  2.,  10.,   2.,   3.])
+    array([0.,  1.,  2.,  3.])
+
+    """
+    from .polynomial import polyadd, polysub, polymulx
+
+    [c] = pu.as_series([c])
+    n = len(c)
+    if n == 1:
+        return c
+    if n == 2:
+        return c
+    else:
+        c0 = c[-2]
+        c1 = c[-1]
+        # i is the current degree of c1
+        for i in range(n - 1, 1, -1):
+            tmp = c0
+            c0 = polysub(c[i - 2], c1*(i - 1))
+            c1 = polyadd(tmp, polymulx(c1))
+        return polyadd(c0, polymulx(c1))
+
+#
+# These are constant arrays are of integer type so as to be compatible
+# with the widest range of other types, such as Decimal.
+#
+
+# Hermite
+hermedomain = np.array([-1, 1])
+
+# Hermite coefficients representing zero.
+hermezero = np.array([0])
+
+# Hermite coefficients representing one.
+hermeone = np.array([1])
+
+# Hermite coefficients representing the identity x.
+hermex = np.array([0, 1])
+
+
+def hermeline(off, scl):
+    """
+    Hermite series whose graph is a straight line.
+
+    Parameters
+    ----------
+    off, scl : scalars
+        The specified line is given by ``off + scl*x``.
+
+    Returns
+    -------
+    y : ndarray
+        This module's representation of the Hermite series for
+        ``off + scl*x``.
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyline
+    numpy.polynomial.chebyshev.chebline
+    numpy.polynomial.legendre.legline
+    numpy.polynomial.laguerre.lagline
+    numpy.polynomial.hermite.hermline
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermeline
+    >>> from numpy.polynomial.hermite_e import hermeline, hermeval
+    >>> hermeval(0,hermeline(3, 2))
+    3.0
+    >>> hermeval(1,hermeline(3, 2))
+    5.0
+
+    """
+    if scl != 0:
+        return np.array([off, scl])
+    else:
+        return np.array([off])
+
+
+def hermefromroots(roots):
+    """
+    Generate a HermiteE series with given roots.
+
+    The function returns the coefficients of the polynomial
+
+    .. math:: p(x) = (x - r_0) * (x - r_1) * ... * (x - r_n),
+
+    in HermiteE form, where the `r_n` are the roots specified in `roots`.
+    If a zero has multiplicity n, then it must appear in `roots` n times.
+    For instance, if 2 is a root of multiplicity three and 3 is a root of
+    multiplicity 2, then `roots` looks something like [2, 2, 2, 3, 3]. The
+    roots can appear in any order.
+
+    If the returned coefficients are `c`, then
+
+    .. math:: p(x) = c_0 + c_1 * He_1(x) + ... +  c_n * He_n(x)
+
+    The coefficient of the last term is not generally 1 for monic
+    polynomials in HermiteE form.
+
+    Parameters
+    ----------
+    roots : array_like
+        Sequence containing the roots.
+
+    Returns
+    -------
+    out : ndarray
+        1-D array of coefficients.  If all roots are real then `out` is a
+        real array, if some of the roots are complex, then `out` is complex
+        even if all the coefficients in the result are real (see Examples
+        below).
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyfromroots
+    numpy.polynomial.legendre.legfromroots
+    numpy.polynomial.laguerre.lagfromroots
+    numpy.polynomial.hermite.hermfromroots
+    numpy.polynomial.chebyshev.chebfromroots
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermefromroots, hermeval
+    >>> coef = hermefromroots((-1, 0, 1))
+    >>> hermeval((-1, 0, 1), coef)
+    array([0., 0., 0.])
+    >>> coef = hermefromroots((-1j, 1j))
+    >>> hermeval((-1j, 1j), coef)
+    array([0.+0.j, 0.+0.j])
+
+    """
+    return pu._fromroots(hermeline, hermemul, roots)
+
+
+def hermeadd(c1, c2):
+    """
+    Add one Hermite series to another.
+
+    Returns the sum of two Hermite series `c1` + `c2`.  The arguments
+    are sequences of coefficients ordered from lowest order term to
+    highest, i.e., [1,2,3] represents the series ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Hermite series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Array representing the Hermite series of their sum.
+
+    See Also
+    --------
+    hermesub, hermemulx, hermemul, hermediv, hermepow
+
+    Notes
+    -----
+    Unlike multiplication, division, etc., the sum of two Hermite series
+    is a Hermite series (without having to "reproject" the result onto
+    the basis set) so addition, just like that of "standard" polynomials,
+    is simply "component-wise."
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermeadd
+    >>> hermeadd([1, 2, 3], [1, 2, 3, 4])
+    array([2.,  4.,  6.,  4.])
+
+    """
+    return pu._add(c1, c2)
+
+
+def hermesub(c1, c2):
+    """
+    Subtract one Hermite series from another.
+
+    Returns the difference of two Hermite series `c1` - `c2`.  The
+    sequences of coefficients are from lowest order term to highest, i.e.,
+    [1,2,3] represents the series ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Hermite series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Of Hermite series coefficients representing their difference.
+
+    See Also
+    --------
+    hermeadd, hermemulx, hermemul, hermediv, hermepow
+
+    Notes
+    -----
+    Unlike multiplication, division, etc., the difference of two Hermite
+    series is a Hermite series (without having to "reproject" the result
+    onto the basis set) so subtraction, just like that of "standard"
+    polynomials, is simply "component-wise."
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermesub
+    >>> hermesub([1, 2, 3, 4], [1, 2, 3])
+    array([0., 0., 0., 4.])
+
+    """
+    return pu._sub(c1, c2)
+
+
+def hermemulx(c):
+    """Multiply a Hermite series by x.
+
+    Multiply the Hermite series `c` by x, where x is the independent
+    variable.
+
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Hermite series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Array representing the result of the multiplication.
+
+    Notes
+    -----
+    The multiplication uses the recursion relationship for Hermite
+    polynomials in the form
+
+    .. math::
+
+        xP_i(x) = (P_{i + 1}(x) + iP_{i - 1}(x)))
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermemulx
+    >>> hermemulx([1, 2, 3])
+    array([2.,  7.,  2.,  3.])
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    # The zero series needs special treatment
+    if len(c) == 1 and c[0] == 0:
+        return c
+
+    prd = np.empty(len(c) + 1, dtype=c.dtype)
+    prd[0] = c[0]*0
+    prd[1] = c[0]
+    for i in range(1, len(c)):
+        prd[i + 1] = c[i]
+        prd[i - 1] += c[i]*i
+    return prd
+
+
+def hermemul(c1, c2):
+    """
+    Multiply one Hermite series by another.
+
+    Returns the product of two Hermite series `c1` * `c2`.  The arguments
+    are sequences of coefficients, from lowest order "term" to highest,
+    e.g., [1,2,3] represents the series ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Hermite series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Of Hermite series coefficients representing their product.
+
+    See Also
+    --------
+    hermeadd, hermesub, hermemulx, hermediv, hermepow
+
+    Notes
+    -----
+    In general, the (polynomial) product of two C-series results in terms
+    that are not in the Hermite polynomial basis set.  Thus, to express
+    the product as a Hermite series, it is necessary to "reproject" the
+    product onto said basis set, which may produce "unintuitive" (but
+    correct) results; see Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermemul
+    >>> hermemul([1, 2, 3], [0, 1, 2])
+    array([14.,  15.,  28.,   7.,   6.])
+
+    """
+    # s1, s2 are trimmed copies
+    [c1, c2] = pu.as_series([c1, c2])
+
+    if len(c1) > len(c2):
+        c = c2
+        xs = c1
+    else:
+        c = c1
+        xs = c2
+
+    if len(c) == 1:
+        c0 = c[0]*xs
+        c1 = 0
+    elif len(c) == 2:
+        c0 = c[0]*xs
+        c1 = c[1]*xs
+    else:
+        nd = len(c)
+        c0 = c[-2]*xs
+        c1 = c[-1]*xs
+        for i in range(3, len(c) + 1):
+            tmp = c0
+            nd = nd - 1
+            c0 = hermesub(c[-i]*xs, c1*(nd - 1))
+            c1 = hermeadd(tmp, hermemulx(c1))
+    return hermeadd(c0, hermemulx(c1))
+
+
+def hermediv(c1, c2):
+    """
+    Divide one Hermite series by another.
+
+    Returns the quotient-with-remainder of two Hermite series
+    `c1` / `c2`.  The arguments are sequences of coefficients from lowest
+    order "term" to highest, e.g., [1,2,3] represents the series
+    ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Hermite series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    [quo, rem] : ndarrays
+        Of Hermite series coefficients representing the quotient and
+        remainder.
+
+    See Also
+    --------
+    hermeadd, hermesub, hermemulx, hermemul, hermepow
+
+    Notes
+    -----
+    In general, the (polynomial) division of one Hermite series by another
+    results in quotient and remainder terms that are not in the Hermite
+    polynomial basis set.  Thus, to express these results as a Hermite
+    series, it is necessary to "reproject" the results onto the Hermite
+    basis set, which may produce "unintuitive" (but correct) results; see
+    Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermediv
+    >>> hermediv([ 14.,  15.,  28.,   7.,   6.], [0, 1, 2])
+    (array([1., 2., 3.]), array([0.]))
+    >>> hermediv([ 15.,  17.,  28.,   7.,   6.], [0, 1, 2])
+    (array([1., 2., 3.]), array([1., 2.]))
+
+    """
+    return pu._div(hermemul, c1, c2)
+
+
+def hermepow(c, pow, maxpower=16):
+    """Raise a Hermite series to a power.
+
+    Returns the Hermite series `c` raised to the power `pow`. The
+    argument `c` is a sequence of coefficients ordered from low to high.
+    i.e., [1,2,3] is the series  ``P_0 + 2*P_1 + 3*P_2.``
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Hermite series coefficients ordered from low to
+        high.
+    pow : integer
+        Power to which the series will be raised
+    maxpower : integer, optional
+        Maximum power allowed. This is mainly to limit growth of the series
+        to unmanageable size. Default is 16
+
+    Returns
+    -------
+    coef : ndarray
+        Hermite series of power.
+
+    See Also
+    --------
+    hermeadd, hermesub, hermemulx, hermemul, hermediv
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermepow
+    >>> hermepow([1, 2, 3], 2)
+    array([23.,  28.,  46.,  12.,   9.])
+
+    """
+    return pu._pow(hermemul, c, pow, maxpower)
+
+
+def hermeder(c, m=1, scl=1, axis=0):
+    """
+    Differentiate a Hermite_e series.
+
+    Returns the series coefficients `c` differentiated `m` times along
+    `axis`.  At each iteration the result is multiplied by `scl` (the
+    scaling factor is for use in a linear change of variable). The argument
+    `c` is an array of coefficients from low to high degree along each
+    axis, e.g., [1,2,3] represents the series ``1*He_0 + 2*He_1 + 3*He_2``
+    while [[1,2],[1,2]] represents ``1*He_0(x)*He_0(y) + 1*He_1(x)*He_0(y)
+    + 2*He_0(x)*He_1(y) + 2*He_1(x)*He_1(y)`` if axis=0 is ``x`` and axis=1
+    is ``y``.
+
+    Parameters
+    ----------
+    c : array_like
+        Array of Hermite_e series coefficients. If `c` is multidimensional
+        the different axis correspond to different variables with the
+        degree in each axis given by the corresponding index.
+    m : int, optional
+        Number of derivatives taken, must be non-negative. (Default: 1)
+    scl : scalar, optional
+        Each differentiation is multiplied by `scl`.  The end result is
+        multiplication by ``scl**m``.  This is for use in a linear change of
+        variable. (Default: 1)
+    axis : int, optional
+        Axis over which the derivative is taken. (Default: 0).
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    der : ndarray
+        Hermite series of the derivative.
+
+    See Also
+    --------
+    hermeint
+
+    Notes
+    -----
+    In general, the result of differentiating a Hermite series does not
+    resemble the same operation on a power series. Thus the result of this
+    function may be "unintuitive," albeit correct; see Examples section
+    below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermeder
+    >>> hermeder([ 1.,  1.,  1.,  1.])
+    array([1.,  2.,  3.])
+    >>> hermeder([-0.25,  1.,  1./2.,  1./3.,  1./4 ], m=2)
+    array([1.,  2.,  3.])
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    cnt = pu._deprecate_as_int(m, "the order of derivation")
+    iaxis = pu._deprecate_as_int(axis, "the axis")
+    if cnt < 0:
+        raise ValueError("The order of derivation must be non-negative")
+    iaxis = normalize_axis_index(iaxis, c.ndim)
+
+    if cnt == 0:
+        return c
+
+    c = np.moveaxis(c, iaxis, 0)
+    n = len(c)
+    if cnt >= n:
+        return c[:1]*0
+    else:
+        for i in range(cnt):
+            n = n - 1
+            c *= scl
+            der = np.empty((n,) + c.shape[1:], dtype=c.dtype)
+            for j in range(n, 0, -1):
+                der[j - 1] = j*c[j]
+            c = der
+    c = np.moveaxis(c, 0, iaxis)
+    return c
+
+
+def hermeint(c, m=1, k=[], lbnd=0, scl=1, axis=0):
+    """
+    Integrate a Hermite_e series.
+
+    Returns the Hermite_e series coefficients `c` integrated `m` times from
+    `lbnd` along `axis`. At each iteration the resulting series is
+    **multiplied** by `scl` and an integration constant, `k`, is added.
+    The scaling factor is for use in a linear change of variable.  ("Buyer
+    beware": note that, depending on what one is doing, one may want `scl`
+    to be the reciprocal of what one might expect; for more information,
+    see the Notes section below.)  The argument `c` is an array of
+    coefficients from low to high degree along each axis, e.g., [1,2,3]
+    represents the series ``H_0 + 2*H_1 + 3*H_2`` while [[1,2],[1,2]]
+    represents ``1*H_0(x)*H_0(y) + 1*H_1(x)*H_0(y) + 2*H_0(x)*H_1(y) +
+    2*H_1(x)*H_1(y)`` if axis=0 is ``x`` and axis=1 is ``y``.
+
+    Parameters
+    ----------
+    c : array_like
+        Array of Hermite_e series coefficients. If c is multidimensional
+        the different axis correspond to different variables with the
+        degree in each axis given by the corresponding index.
+    m : int, optional
+        Order of integration, must be positive. (Default: 1)
+    k : {[], list, scalar}, optional
+        Integration constant(s).  The value of the first integral at
+        ``lbnd`` is the first value in the list, the value of the second
+        integral at ``lbnd`` is the second value, etc.  If ``k == []`` (the
+        default), all constants are set to zero.  If ``m == 1``, a single
+        scalar can be given instead of a list.
+    lbnd : scalar, optional
+        The lower bound of the integral. (Default: 0)
+    scl : scalar, optional
+        Following each integration the result is *multiplied* by `scl`
+        before the integration constant is added. (Default: 1)
+    axis : int, optional
+        Axis over which the integral is taken. (Default: 0).
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    S : ndarray
+        Hermite_e series coefficients of the integral.
+
+    Raises
+    ------
+    ValueError
+        If ``m < 0``, ``len(k) > m``, ``np.ndim(lbnd) != 0``, or
+        ``np.ndim(scl) != 0``.
+
+    See Also
+    --------
+    hermeder
+
+    Notes
+    -----
+    Note that the result of each integration is *multiplied* by `scl`.
+    Why is this important to note?  Say one is making a linear change of
+    variable :math:`u = ax + b` in an integral relative to `x`.  Then
+    :math:`dx = du/a`, so one will need to set `scl` equal to
+    :math:`1/a` - perhaps not what one would have first thought.
+
+    Also note that, in general, the result of integrating a C-series needs
+    to be "reprojected" onto the C-series basis set.  Thus, typically,
+    the result of this function is "unintuitive," albeit correct; see
+    Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermeint
+    >>> hermeint([1, 2, 3]) # integrate once, value 0 at 0.
+    array([1., 1., 1., 1.])
+    >>> hermeint([1, 2, 3], m=2) # integrate twice, value & deriv 0 at 0
+    array([-0.25      ,  1.        ,  0.5       ,  0.33333333,  0.25      ]) # may vary
+    >>> hermeint([1, 2, 3], k=1) # integrate once, value 1 at 0.
+    array([2., 1., 1., 1.])
+    >>> hermeint([1, 2, 3], lbnd=-1) # integrate once, value 0 at -1
+    array([-1.,  1.,  1.,  1.])
+    >>> hermeint([1, 2, 3], m=2, k=[1, 2], lbnd=-1)
+    array([ 1.83333333,  0.        ,  0.5       ,  0.33333333,  0.25      ]) # may vary
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    if not np.iterable(k):
+        k = [k]
+    cnt = pu._deprecate_as_int(m, "the order of integration")
+    iaxis = pu._deprecate_as_int(axis, "the axis")
+    if cnt < 0:
+        raise ValueError("The order of integration must be non-negative")
+    if len(k) > cnt:
+        raise ValueError("Too many integration constants")
+    if np.ndim(lbnd) != 0:
+        raise ValueError("lbnd must be a scalar.")
+    if np.ndim(scl) != 0:
+        raise ValueError("scl must be a scalar.")
+    iaxis = normalize_axis_index(iaxis, c.ndim)
+
+    if cnt == 0:
+        return c
+
+    c = np.moveaxis(c, iaxis, 0)
+    k = list(k) + [0]*(cnt - len(k))
+    for i in range(cnt):
+        n = len(c)
+        c *= scl
+        if n == 1 and np.all(c[0] == 0):
+            c[0] += k[i]
+        else:
+            tmp = np.empty((n + 1,) + c.shape[1:], dtype=c.dtype)
+            tmp[0] = c[0]*0
+            tmp[1] = c[0]
+            for j in range(1, n):
+                tmp[j + 1] = c[j]/(j + 1)
+            tmp[0] += k[i] - hermeval(lbnd, tmp)
+            c = tmp
+    c = np.moveaxis(c, 0, iaxis)
+    return c
+
+
+def hermeval(x, c, tensor=True):
+    """
+    Evaluate an HermiteE series at points x.
+
+    If `c` is of length `n + 1`, this function returns the value:
+
+    .. math:: p(x) = c_0 * He_0(x) + c_1 * He_1(x) + ... + c_n * He_n(x)
+
+    The parameter `x` is converted to an array only if it is a tuple or a
+    list, otherwise it is treated as a scalar. In either case, either `x`
+    or its elements must support multiplication and addition both with
+    themselves and with the elements of `c`.
+
+    If `c` is a 1-D array, then `p(x)` will have the same shape as `x`.  If
+    `c` is multidimensional, then the shape of the result depends on the
+    value of `tensor`. If `tensor` is true the shape will be c.shape[1:] +
+    x.shape. If `tensor` is false the shape will be c.shape[1:]. Note that
+    scalars have shape (,).
+
+    Trailing zeros in the coefficients will be used in the evaluation, so
+    they should be avoided if efficiency is a concern.
+
+    Parameters
+    ----------
+    x : array_like, compatible object
+        If `x` is a list or tuple, it is converted to an ndarray, otherwise
+        it is left unchanged and treated as a scalar. In either case, `x`
+        or its elements must support addition and multiplication with
+        with themselves and with the elements of `c`.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree n are contained in c[n]. If `c` is multidimensional the
+        remaining indices enumerate multiple polynomials. In the two
+        dimensional case the coefficients may be thought of as stored in
+        the columns of `c`.
+    tensor : boolean, optional
+        If True, the shape of the coefficient array is extended with ones
+        on the right, one for each dimension of `x`. Scalars have dimension 0
+        for this action. The result is that every column of coefficients in
+        `c` is evaluated for every element of `x`. If False, `x` is broadcast
+        over the columns of `c` for the evaluation.  This keyword is useful
+        when `c` is multidimensional. The default value is True.
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    values : ndarray, algebra_like
+        The shape of the return value is described above.
+
+    See Also
+    --------
+    hermeval2d, hermegrid2d, hermeval3d, hermegrid3d
+
+    Notes
+    -----
+    The evaluation uses Clenshaw recursion, aka synthetic division.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermeval
+    >>> coef = [1,2,3]
+    >>> hermeval(1, coef)
+    3.0
+    >>> hermeval([[1,2],[3,4]], coef)
+    array([[ 3., 14.],
+           [31., 54.]])
+
+    """
+    c = np.array(c, ndmin=1, copy=False)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    if isinstance(x, (tuple, list)):
+        x = np.asarray(x)
+    if isinstance(x, np.ndarray) and tensor:
+        c = c.reshape(c.shape + (1,)*x.ndim)
+
+    if len(c) == 1:
+        c0 = c[0]
+        c1 = 0
+    elif len(c) == 2:
+        c0 = c[0]
+        c1 = c[1]
+    else:
+        nd = len(c)
+        c0 = c[-2]
+        c1 = c[-1]
+        for i in range(3, len(c) + 1):
+            tmp = c0
+            nd = nd - 1
+            c0 = c[-i] - c1*(nd - 1)
+            c1 = tmp + c1*x
+    return c0 + c1*x
+
+
+def hermeval2d(x, y, c):
+    """
+    Evaluate a 2-D HermiteE series at points (x, y).
+
+    This function returns the values:
+
+    .. math:: p(x,y) = \\sum_{i,j} c_{i,j} * He_i(x) * He_j(y)
+
+    The parameters `x` and `y` are converted to arrays only if they are
+    tuples or a lists, otherwise they are treated as a scalars and they
+    must have the same shape after conversion. In either case, either `x`
+    and `y` or their elements must support multiplication and addition both
+    with themselves and with the elements of `c`.
+
+    If `c` is a 1-D array a one is implicitly appended to its shape to make
+    it 2-D. The shape of the result will be c.shape[2:] + x.shape.
+
+    Parameters
+    ----------
+    x, y : array_like, compatible objects
+        The two dimensional series is evaluated at the points `(x, y)`,
+        where `x` and `y` must have the same shape. If `x` or `y` is a list
+        or tuple, it is first converted to an ndarray, otherwise it is left
+        unchanged and if it isn't an ndarray it is treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term
+        of multi-degree i,j is contained in ``c[i,j]``. If `c` has
+        dimension greater than two the remaining indices enumerate multiple
+        sets of coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points formed with
+        pairs of corresponding values from `x` and `y`.
+
+    See Also
+    --------
+    hermeval, hermegrid2d, hermeval3d, hermegrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._valnd(hermeval, c, x, y)
+
+
+def hermegrid2d(x, y, c):
+    """
+    Evaluate a 2-D HermiteE series on the Cartesian product of x and y.
+
+    This function returns the values:
+
+    .. math:: p(a,b) = \\sum_{i,j} c_{i,j} * H_i(a) * H_j(b)
+
+    where the points `(a, b)` consist of all pairs formed by taking
+    `a` from `x` and `b` from `y`. The resulting points form a grid with
+    `x` in the first dimension and `y` in the second.
+
+    The parameters `x` and `y` are converted to arrays only if they are
+    tuples or a lists, otherwise they are treated as a scalars. In either
+    case, either `x` and `y` or their elements must support multiplication
+    and addition both with themselves and with the elements of `c`.
+
+    If `c` has fewer than two dimensions, ones are implicitly appended to
+    its shape to make it 2-D. The shape of the result will be c.shape[2:] +
+    x.shape.
+
+    Parameters
+    ----------
+    x, y : array_like, compatible objects
+        The two dimensional series is evaluated at the points in the
+        Cartesian product of `x` and `y`.  If `x` or `y` is a list or
+        tuple, it is first converted to an ndarray, otherwise it is left
+        unchanged and, if it isn't an ndarray, it is treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree i,j are contained in ``c[i,j]``. If `c` has dimension
+        greater than two the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points in the Cartesian
+        product of `x` and `y`.
+
+    See Also
+    --------
+    hermeval, hermeval2d, hermeval3d, hermegrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._gridnd(hermeval, c, x, y)
+
+
+def hermeval3d(x, y, z, c):
+    """
+    Evaluate a 3-D Hermite_e series at points (x, y, z).
+
+    This function returns the values:
+
+    .. math:: p(x,y,z) = \\sum_{i,j,k} c_{i,j,k} * He_i(x) * He_j(y) * He_k(z)
+
+    The parameters `x`, `y`, and `z` are converted to arrays only if
+    they are tuples or a lists, otherwise they are treated as a scalars and
+    they must have the same shape after conversion. In either case, either
+    `x`, `y`, and `z` or their elements must support multiplication and
+    addition both with themselves and with the elements of `c`.
+
+    If `c` has fewer than 3 dimensions, ones are implicitly appended to its
+    shape to make it 3-D. The shape of the result will be c.shape[3:] +
+    x.shape.
+
+    Parameters
+    ----------
+    x, y, z : array_like, compatible object
+        The three dimensional series is evaluated at the points
+        `(x, y, z)`, where `x`, `y`, and `z` must have the same shape.  If
+        any of `x`, `y`, or `z` is a list or tuple, it is first converted
+        to an ndarray, otherwise it is left unchanged and if it isn't an
+        ndarray it is  treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term of
+        multi-degree i,j,k is contained in ``c[i,j,k]``. If `c` has dimension
+        greater than 3 the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the multidimensional polynomial on points formed with
+        triples of corresponding values from `x`, `y`, and `z`.
+
+    See Also
+    --------
+    hermeval, hermeval2d, hermegrid2d, hermegrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._valnd(hermeval, c, x, y, z)
+
+
+def hermegrid3d(x, y, z, c):
+    """
+    Evaluate a 3-D HermiteE series on the Cartesian product of x, y, and z.
+
+    This function returns the values:
+
+    .. math:: p(a,b,c) = \\sum_{i,j,k} c_{i,j,k} * He_i(a) * He_j(b) * He_k(c)
+
+    where the points `(a, b, c)` consist of all triples formed by taking
+    `a` from `x`, `b` from `y`, and `c` from `z`. The resulting points form
+    a grid with `x` in the first dimension, `y` in the second, and `z` in
+    the third.
+
+    The parameters `x`, `y`, and `z` are converted to arrays only if they
+    are tuples or a lists, otherwise they are treated as a scalars. In
+    either case, either `x`, `y`, and `z` or their elements must support
+    multiplication and addition both with themselves and with the elements
+    of `c`.
+
+    If `c` has fewer than three dimensions, ones are implicitly appended to
+    its shape to make it 3-D. The shape of the result will be c.shape[3:] +
+    x.shape + y.shape + z.shape.
+
+    Parameters
+    ----------
+    x, y, z : array_like, compatible objects
+        The three dimensional series is evaluated at the points in the
+        Cartesian product of `x`, `y`, and `z`.  If `x`,`y`, or `z` is a
+        list or tuple, it is first converted to an ndarray, otherwise it is
+        left unchanged and, if it isn't an ndarray, it is treated as a
+        scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree i,j are contained in ``c[i,j]``. If `c` has dimension
+        greater than two the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points in the Cartesian
+        product of `x` and `y`.
+
+    See Also
+    --------
+    hermeval, hermeval2d, hermegrid2d, hermeval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._gridnd(hermeval, c, x, y, z)
+
+
+def hermevander(x, deg):
+    """Pseudo-Vandermonde matrix of given degree.
+
+    Returns the pseudo-Vandermonde matrix of degree `deg` and sample points
+    `x`. The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., i] = He_i(x),
+
+    where `0 <= i <= deg`. The leading indices of `V` index the elements of
+    `x` and the last index is the degree of the HermiteE polynomial.
+
+    If `c` is a 1-D array of coefficients of length `n + 1` and `V` is the
+    array ``V = hermevander(x, n)``, then ``np.dot(V, c)`` and
+    ``hermeval(x, c)`` are the same up to roundoff. This equivalence is
+    useful both for least squares fitting and for the evaluation of a large
+    number of HermiteE series of the same degree and sample points.
+
+    Parameters
+    ----------
+    x : array_like
+        Array of points. The dtype is converted to float64 or complex128
+        depending on whether any of the elements are complex. If `x` is
+        scalar it is converted to a 1-D array.
+    deg : int
+        Degree of the resulting matrix.
+
+    Returns
+    -------
+    vander : ndarray
+        The pseudo-Vandermonde matrix. The shape of the returned matrix is
+        ``x.shape + (deg + 1,)``, where The last index is the degree of the
+        corresponding HermiteE polynomial.  The dtype will be the same as
+        the converted `x`.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermevander
+    >>> x = np.array([-1, 0, 1])
+    >>> hermevander(x, 3)
+    array([[ 1., -1.,  0.,  2.],
+           [ 1.,  0., -1., -0.],
+           [ 1.,  1.,  0., -2.]])
+
+    """
+    ideg = pu._deprecate_as_int(deg, "deg")
+    if ideg < 0:
+        raise ValueError("deg must be non-negative")
+
+    x = np.array(x, copy=False, ndmin=1) + 0.0
+    dims = (ideg + 1,) + x.shape
+    dtyp = x.dtype
+    v = np.empty(dims, dtype=dtyp)
+    v[0] = x*0 + 1
+    if ideg > 0:
+        v[1] = x
+        for i in range(2, ideg + 1):
+            v[i] = (v[i-1]*x - v[i-2]*(i - 1))
+    return np.moveaxis(v, 0, -1)
+
+
+def hermevander2d(x, y, deg):
+    """Pseudo-Vandermonde matrix of given degrees.
+
+    Returns the pseudo-Vandermonde matrix of degrees `deg` and sample
+    points `(x, y)`. The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., (deg[1] + 1)*i + j] = He_i(x) * He_j(y),
+
+    where `0 <= i <= deg[0]` and `0 <= j <= deg[1]`. The leading indices of
+    `V` index the points `(x, y)` and the last index encodes the degrees of
+    the HermiteE polynomials.
+
+    If ``V = hermevander2d(x, y, [xdeg, ydeg])``, then the columns of `V`
+    correspond to the elements of a 2-D coefficient array `c` of shape
+    (xdeg + 1, ydeg + 1) in the order
+
+    .. math:: c_{00}, c_{01}, c_{02} ... , c_{10}, c_{11}, c_{12} ...
+
+    and ``np.dot(V, c.flat)`` and ``hermeval2d(x, y, c)`` will be the same
+    up to roundoff. This equivalence is useful both for least squares
+    fitting and for the evaluation of a large number of 2-D HermiteE
+    series of the same degrees and sample points.
+
+    Parameters
+    ----------
+    x, y : array_like
+        Arrays of point coordinates, all of the same shape. The dtypes
+        will be converted to either float64 or complex128 depending on
+        whether any of the elements are complex. Scalars are converted to
+        1-D arrays.
+    deg : list of ints
+        List of maximum degrees of the form [x_deg, y_deg].
+
+    Returns
+    -------
+    vander2d : ndarray
+        The shape of the returned matrix is ``x.shape + (order,)``, where
+        :math:`order = (deg[0]+1)*(deg[1]+1)`.  The dtype will be the same
+        as the converted `x` and `y`.
+
+    See Also
+    --------
+    hermevander, hermevander3d, hermeval2d, hermeval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._vander_nd_flat((hermevander, hermevander), (x, y), deg)
+
+
+def hermevander3d(x, y, z, deg):
+    """Pseudo-Vandermonde matrix of given degrees.
+
+    Returns the pseudo-Vandermonde matrix of degrees `deg` and sample
+    points `(x, y, z)`. If `l, m, n` are the given degrees in `x, y, z`,
+    then Hehe pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., (m+1)(n+1)i + (n+1)j + k] = He_i(x)*He_j(y)*He_k(z),
+
+    where `0 <= i <= l`, `0 <= j <= m`, and `0 <= j <= n`.  The leading
+    indices of `V` index the points `(x, y, z)` and the last index encodes
+    the degrees of the HermiteE polynomials.
+
+    If ``V = hermevander3d(x, y, z, [xdeg, ydeg, zdeg])``, then the columns
+    of `V` correspond to the elements of a 3-D coefficient array `c` of
+    shape (xdeg + 1, ydeg + 1, zdeg + 1) in the order
+
+    .. math:: c_{000}, c_{001}, c_{002},... , c_{010}, c_{011}, c_{012},...
+
+    and  ``np.dot(V, c.flat)`` and ``hermeval3d(x, y, z, c)`` will be the
+    same up to roundoff. This equivalence is useful both for least squares
+    fitting and for the evaluation of a large number of 3-D HermiteE
+    series of the same degrees and sample points.
+
+    Parameters
+    ----------
+    x, y, z : array_like
+        Arrays of point coordinates, all of the same shape. The dtypes will
+        be converted to either float64 or complex128 depending on whether
+        any of the elements are complex. Scalars are converted to 1-D
+        arrays.
+    deg : list of ints
+        List of maximum degrees of the form [x_deg, y_deg, z_deg].
+
+    Returns
+    -------
+    vander3d : ndarray
+        The shape of the returned matrix is ``x.shape + (order,)``, where
+        :math:`order = (deg[0]+1)*(deg[1]+1)*(deg[2]+1)`.  The dtype will
+        be the same as the converted `x`, `y`, and `z`.
+
+    See Also
+    --------
+    hermevander, hermevander3d, hermeval2d, hermeval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._vander_nd_flat((hermevander, hermevander, hermevander), (x, y, z), deg)
+
+
+def hermefit(x, y, deg, rcond=None, full=False, w=None):
+    """
+    Least squares fit of Hermite series to data.
+
+    Return the coefficients of a HermiteE series of degree `deg` that is
+    the least squares fit to the data values `y` given at points `x`. If
+    `y` is 1-D the returned coefficients will also be 1-D. If `y` is 2-D
+    multiple fits are done, one for each column of `y`, and the resulting
+    coefficients are stored in the corresponding columns of a 2-D return.
+    The fitted polynomial(s) are in the form
+
+    .. math::  p(x) = c_0 + c_1 * He_1(x) + ... + c_n * He_n(x),
+
+    where `n` is `deg`.
+
+    Parameters
+    ----------
+    x : array_like, shape (M,)
+        x-coordinates of the M sample points ``(x[i], y[i])``.
+    y : array_like, shape (M,) or (M, K)
+        y-coordinates of the sample points. Several data sets of sample
+        points sharing the same x-coordinates can be fitted at once by
+        passing in a 2D-array that contains one dataset per column.
+    deg : int or 1-D array_like
+        Degree(s) of the fitting polynomials. If `deg` is a single integer
+        all terms up to and including the `deg`'th term are included in the
+        fit. For NumPy versions >= 1.11.0 a list of integers specifying the
+        degrees of the terms to include may be used instead.
+    rcond : float, optional
+        Relative condition number of the fit. Singular values smaller than
+        this relative to the largest singular value will be ignored. The
+        default value is len(x)*eps, where eps is the relative precision of
+        the float type, about 2e-16 in most cases.
+    full : bool, optional
+        Switch determining nature of return value. When it is False (the
+        default) just the coefficients are returned, when True diagnostic
+        information from the singular value decomposition is also returned.
+    w : array_like, shape (`M`,), optional
+        Weights. If not None, the weight ``w[i]`` applies to the unsquared
+        residual ``y[i] - y_hat[i]`` at ``x[i]``. Ideally the weights are
+        chosen so that the errors of the products ``w[i]*y[i]`` all have the
+        same variance.  When using inverse-variance weighting, use
+        ``w[i] = 1/sigma(y[i])``.  The default value is None.
+
+    Returns
+    -------
+    coef : ndarray, shape (M,) or (M, K)
+        Hermite coefficients ordered from low to high. If `y` was 2-D,
+        the coefficients for the data in column k  of `y` are in column
+        `k`.
+
+    [residuals, rank, singular_values, rcond] : list
+        These values are only returned if ``full == True``
+
+        - residuals -- sum of squared residuals of the least squares fit
+        - rank -- the numerical rank of the scaled Vandermonde matrix
+        - singular_values -- singular values of the scaled Vandermonde matrix
+        - rcond -- value of `rcond`.
+
+        For more details, see `numpy.linalg.lstsq`.
+
+    Warns
+    -----
+    RankWarning
+        The rank of the coefficient matrix in the least-squares fit is
+        deficient. The warning is only raised if ``full = False``.  The
+        warnings can be turned off by
+
+        >>> import warnings
+        >>> warnings.simplefilter('ignore', np.RankWarning)
+
+    See Also
+    --------
+    numpy.polynomial.chebyshev.chebfit
+    numpy.polynomial.legendre.legfit
+    numpy.polynomial.polynomial.polyfit
+    numpy.polynomial.hermite.hermfit
+    numpy.polynomial.laguerre.lagfit
+    hermeval : Evaluates a Hermite series.
+    hermevander : pseudo Vandermonde matrix of Hermite series.
+    hermeweight : HermiteE weight function.
+    numpy.linalg.lstsq : Computes a least-squares fit from the matrix.
+    scipy.interpolate.UnivariateSpline : Computes spline fits.
+
+    Notes
+    -----
+    The solution is the coefficients of the HermiteE series `p` that
+    minimizes the sum of the weighted squared errors
+
+    .. math:: E = \\sum_j w_j^2 * |y_j - p(x_j)|^2,
+
+    where the :math:`w_j` are the weights. This problem is solved by
+    setting up the (typically) overdetermined matrix equation
+
+    .. math:: V(x) * c = w * y,
+
+    where `V` is the pseudo Vandermonde matrix of `x`, the elements of `c`
+    are the coefficients to be solved for, and the elements of `y` are the
+    observed values.  This equation is then solved using the singular value
+    decomposition of `V`.
+
+    If some of the singular values of `V` are so small that they are
+    neglected, then a `RankWarning` will be issued. This means that the
+    coefficient values may be poorly determined. Using a lower order fit
+    will usually get rid of the warning.  The `rcond` parameter can also be
+    set to a value smaller than its default, but the resulting fit may be
+    spurious and have large contributions from roundoff error.
+
+    Fits using HermiteE series are probably most useful when the data can
+    be approximated by ``sqrt(w(x)) * p(x)``, where `w(x)` is the HermiteE
+    weight. In that case the weight ``sqrt(w(x[i]))`` should be used
+    together with data values ``y[i]/sqrt(w(x[i]))``. The weight function is
+    available as `hermeweight`.
+
+    References
+    ----------
+    .. [1] Wikipedia, "Curve fitting",
+           https://en.wikipedia.org/wiki/Curve_fitting
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermefit, hermeval
+    >>> x = np.linspace(-10, 10)
+    >>> np.random.seed(123)
+    >>> err = np.random.randn(len(x))/10
+    >>> y = hermeval(x, [1, 2, 3]) + err
+    >>> hermefit(x, y, 2)
+    array([ 1.01690445,  1.99951418,  2.99948696]) # may vary
+
+    """
+    return pu._fit(hermevander, x, y, deg, rcond, full, w)
+
+
+def hermecompanion(c):
+    """
+    Return the scaled companion matrix of c.
+
+    The basis polynomials are scaled so that the companion matrix is
+    symmetric when `c` is an HermiteE basis polynomial. This provides
+    better eigenvalue estimates than the unscaled case and for basis
+    polynomials the eigenvalues are guaranteed to be real if
+    `numpy.linalg.eigvalsh` is used to obtain them.
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of HermiteE series coefficients ordered from low to high
+        degree.
+
+    Returns
+    -------
+    mat : ndarray
+        Scaled companion matrix of dimensions (deg, deg).
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    if len(c) < 2:
+        raise ValueError('Series must have maximum degree of at least 1.')
+    if len(c) == 2:
+        return np.array([[-c[0]/c[1]]])
+
+    n = len(c) - 1
+    mat = np.zeros((n, n), dtype=c.dtype)
+    scl = np.hstack((1., 1./np.sqrt(np.arange(n - 1, 0, -1))))
+    scl = np.multiply.accumulate(scl)[::-1]
+    top = mat.reshape(-1)[1::n+1]
+    bot = mat.reshape(-1)[n::n+1]
+    top[...] = np.sqrt(np.arange(1, n))
+    bot[...] = top
+    mat[:, -1] -= scl*c[:-1]/c[-1]
+    return mat
+
+
+def hermeroots(c):
+    """
+    Compute the roots of a HermiteE series.
+
+    Return the roots (a.k.a. "zeros") of the polynomial
+
+    .. math:: p(x) = \\sum_i c[i] * He_i(x).
+
+    Parameters
+    ----------
+    c : 1-D array_like
+        1-D array of coefficients.
+
+    Returns
+    -------
+    out : ndarray
+        Array of the roots of the series. If all the roots are real,
+        then `out` is also real, otherwise it is complex.
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyroots
+    numpy.polynomial.legendre.legroots
+    numpy.polynomial.laguerre.lagroots
+    numpy.polynomial.hermite.hermroots
+    numpy.polynomial.chebyshev.chebroots
+
+    Notes
+    -----
+    The root estimates are obtained as the eigenvalues of the companion
+    matrix, Roots far from the origin of the complex plane may have large
+    errors due to the numerical instability of the series for such
+    values. Roots with multiplicity greater than 1 will also show larger
+    errors as the value of the series near such points is relatively
+    insensitive to errors in the roots. Isolated roots near the origin can
+    be improved by a few iterations of Newton's method.
+
+    The HermiteE series basis polynomials aren't powers of `x` so the
+    results of this function may seem unintuitive.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.hermite_e import hermeroots, hermefromroots
+    >>> coef = hermefromroots([-1, 0, 1])
+    >>> coef
+    array([0., 2., 0., 1.])
+    >>> hermeroots(coef)
+    array([-1.,  0.,  1.]) # may vary
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    if len(c) <= 1:
+        return np.array([], dtype=c.dtype)
+    if len(c) == 2:
+        return np.array([-c[0]/c[1]])
+
+    # rotated companion matrix reduces error
+    m = hermecompanion(c)[::-1,::-1]
+    r = la.eigvals(m)
+    r.sort()
+    return r
+
+
+def _normed_hermite_e_n(x, n):
+    """
+    Evaluate a normalized HermiteE polynomial.
+
+    Compute the value of the normalized HermiteE polynomial of degree ``n``
+    at the points ``x``.
+
+
+    Parameters
+    ----------
+    x : ndarray of double.
+        Points at which to evaluate the function
+    n : int
+        Degree of the normalized HermiteE function to be evaluated.
+
+    Returns
+    -------
+    values : ndarray
+        The shape of the return value is described above.
+
+    Notes
+    -----
+    .. versionadded:: 1.10.0
+
+    This function is needed for finding the Gauss points and integration
+    weights for high degrees. The values of the standard HermiteE functions
+    overflow when n >= 207.
+
+    """
+    if n == 0:
+        return np.full(x.shape, 1/np.sqrt(np.sqrt(2*np.pi)))
+
+    c0 = 0.
+    c1 = 1./np.sqrt(np.sqrt(2*np.pi))
+    nd = float(n)
+    for i in range(n - 1):
+        tmp = c0
+        c0 = -c1*np.sqrt((nd - 1.)/nd)
+        c1 = tmp + c1*x*np.sqrt(1./nd)
+        nd = nd - 1.0
+    return c0 + c1*x
+
+
+def hermegauss(deg):
+    """
+    Gauss-HermiteE quadrature.
+
+    Computes the sample points and weights for Gauss-HermiteE quadrature.
+    These sample points and weights will correctly integrate polynomials of
+    degree :math:`2*deg - 1` or less over the interval :math:`[-\\inf, \\inf]`
+    with the weight function :math:`f(x) = \\exp(-x^2/2)`.
+
+    Parameters
+    ----------
+    deg : int
+        Number of sample points and weights. It must be >= 1.
+
+    Returns
+    -------
+    x : ndarray
+        1-D ndarray containing the sample points.
+    y : ndarray
+        1-D ndarray containing the weights.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    The results have only been tested up to degree 100, higher degrees may
+    be problematic. The weights are determined by using the fact that
+
+    .. math:: w_k = c / (He'_n(x_k) * He_{n-1}(x_k))
+
+    where :math:`c` is a constant independent of :math:`k` and :math:`x_k`
+    is the k'th root of :math:`He_n`, and then scaling the results to get
+    the right value when integrating 1.
+
+    """
+    ideg = pu._deprecate_as_int(deg, "deg")
+    if ideg <= 0:
+        raise ValueError("deg must be a positive integer")
+
+    # first approximation of roots. We use the fact that the companion
+    # matrix is symmetric in this case in order to obtain better zeros.
+    c = np.array([0]*deg + [1])
+    m = hermecompanion(c)
+    x = la.eigvalsh(m)
+
+    # improve roots by one application of Newton
+    dy = _normed_hermite_e_n(x, ideg)
+    df = _normed_hermite_e_n(x, ideg - 1) * np.sqrt(ideg)
+    x -= dy/df
+
+    # compute the weights. We scale the factor to avoid possible numerical
+    # overflow.
+    fm = _normed_hermite_e_n(x, ideg - 1)
+    fm /= np.abs(fm).max()
+    w = 1/(fm * fm)
+
+    # for Hermite_e we can also symmetrize
+    w = (w + w[::-1])/2
+    x = (x - x[::-1])/2
+
+    # scale w to get the right value
+    w *= np.sqrt(2*np.pi) / w.sum()
+
+    return x, w
+
+
+def hermeweight(x):
+    """Weight function of the Hermite_e polynomials.
+
+    The weight function is :math:`\\exp(-x^2/2)` and the interval of
+    integration is :math:`[-\\inf, \\inf]`. the HermiteE polynomials are
+    orthogonal, but not normalized, with respect to this weight function.
+
+    Parameters
+    ----------
+    x : array_like
+       Values at which the weight function will be computed.
+
+    Returns
+    -------
+    w : ndarray
+       The weight function at `x`.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    w = np.exp(-.5*x**2)
+    return w
+
+
+#
+# HermiteE series class
+#
+
+class HermiteE(ABCPolyBase):
+    """An HermiteE series class.
+
+    The HermiteE class provides the standard Python numerical methods
+    '+', '-', '*', '//', '%', 'divmod', '**', and '()' as well as the
+    attributes and methods listed in the `ABCPolyBase` documentation.
+
+    Parameters
+    ----------
+    coef : array_like
+        HermiteE coefficients in order of increasing degree, i.e,
+        ``(1, 2, 3)`` gives ``1*He_0(x) + 2*He_1(X) + 3*He_2(x)``.
+    domain : (2,) array_like, optional
+        Domain to use. The interval ``[domain[0], domain[1]]`` is mapped
+        to the interval ``[window[0], window[1]]`` by shifting and scaling.
+        The default value is [-1, 1].
+    window : (2,) array_like, optional
+        Window, see `domain` for its use. The default value is [-1, 1].
+
+        .. versionadded:: 1.6.0
+    symbol : str, optional
+        Symbol used to represent the independent variable in string
+        representations of the polynomial expression, e.g. for printing.
+        The symbol must be a valid Python identifier. Default value is 'x'.
+
+        .. versionadded:: 1.24
+
+    """
+    # Virtual Functions
+    _add = staticmethod(hermeadd)
+    _sub = staticmethod(hermesub)
+    _mul = staticmethod(hermemul)
+    _div = staticmethod(hermediv)
+    _pow = staticmethod(hermepow)
+    _val = staticmethod(hermeval)
+    _int = staticmethod(hermeint)
+    _der = staticmethod(hermeder)
+    _fit = staticmethod(hermefit)
+    _line = staticmethod(hermeline)
+    _roots = staticmethod(hermeroots)
+    _fromroots = staticmethod(hermefromroots)
+
+    # Virtual properties
+    domain = np.array(hermedomain)
+    window = np.array(hermedomain)
+    basis_name = 'He'
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite_e.pyi b/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite_e.pyi
new file mode 100644
index 00000000..0b7152a2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite_e.pyi
@@ -0,0 +1,46 @@
+from typing import Any
+
+from numpy import ndarray, dtype, int_
+from numpy.polynomial._polybase import ABCPolyBase
+from numpy.polynomial.polyutils import trimcoef
+
+__all__: list[str]
+
+hermetrim = trimcoef
+
+def poly2herme(pol): ...
+def herme2poly(c): ...
+
+hermedomain: ndarray[Any, dtype[int_]]
+hermezero: ndarray[Any, dtype[int_]]
+hermeone: ndarray[Any, dtype[int_]]
+hermex: ndarray[Any, dtype[int_]]
+
+def hermeline(off, scl): ...
+def hermefromroots(roots): ...
+def hermeadd(c1, c2): ...
+def hermesub(c1, c2): ...
+def hermemulx(c): ...
+def hermemul(c1, c2): ...
+def hermediv(c1, c2): ...
+def hermepow(c, pow, maxpower=...): ...
+def hermeder(c, m=..., scl=..., axis=...): ...
+def hermeint(c, m=..., k = ..., lbnd=..., scl=..., axis=...): ...
+def hermeval(x, c, tensor=...): ...
+def hermeval2d(x, y, c): ...
+def hermegrid2d(x, y, c): ...
+def hermeval3d(x, y, z, c): ...
+def hermegrid3d(x, y, z, c): ...
+def hermevander(x, deg): ...
+def hermevander2d(x, y, deg): ...
+def hermevander3d(x, y, z, deg): ...
+def hermefit(x, y, deg, rcond=..., full=..., w=...): ...
+def hermecompanion(c): ...
+def hermeroots(c): ...
+def hermegauss(deg): ...
+def hermeweight(x): ...
+
+class HermiteE(ABCPolyBase):
+    domain: Any
+    window: Any
+    basis_name: Any
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/laguerre.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/laguerre.py
new file mode 100644
index 00000000..925d4898
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/laguerre.py
@@ -0,0 +1,1651 @@
+"""
+==================================================
+Laguerre Series (:mod:`numpy.polynomial.laguerre`)
+==================================================
+
+This module provides a number of objects (mostly functions) useful for
+dealing with Laguerre series, including a `Laguerre` class that
+encapsulates the usual arithmetic operations.  (General information
+on how this module represents and works with such polynomials is in the
+docstring for its "parent" sub-package, `numpy.polynomial`).
+
+Classes
+-------
+.. autosummary::
+   :toctree: generated/
+
+   Laguerre
+
+Constants
+---------
+.. autosummary::
+   :toctree: generated/
+
+   lagdomain
+   lagzero
+   lagone
+   lagx
+
+Arithmetic
+----------
+.. autosummary::
+   :toctree: generated/
+
+   lagadd
+   lagsub
+   lagmulx
+   lagmul
+   lagdiv
+   lagpow
+   lagval
+   lagval2d
+   lagval3d
+   laggrid2d
+   laggrid3d
+
+Calculus
+--------
+.. autosummary::
+   :toctree: generated/
+
+   lagder
+   lagint
+
+Misc Functions
+--------------
+.. autosummary::
+   :toctree: generated/
+
+   lagfromroots
+   lagroots
+   lagvander
+   lagvander2d
+   lagvander3d
+   laggauss
+   lagweight
+   lagcompanion
+   lagfit
+   lagtrim
+   lagline
+   lag2poly
+   poly2lag
+
+See also
+--------
+`numpy.polynomial`
+
+"""
+import numpy as np
+import numpy.linalg as la
+from numpy.core.multiarray import normalize_axis_index
+
+from . import polyutils as pu
+from ._polybase import ABCPolyBase
+
+__all__ = [
+    'lagzero', 'lagone', 'lagx', 'lagdomain', 'lagline', 'lagadd',
+    'lagsub', 'lagmulx', 'lagmul', 'lagdiv', 'lagpow', 'lagval', 'lagder',
+    'lagint', 'lag2poly', 'poly2lag', 'lagfromroots', 'lagvander',
+    'lagfit', 'lagtrim', 'lagroots', 'Laguerre', 'lagval2d', 'lagval3d',
+    'laggrid2d', 'laggrid3d', 'lagvander2d', 'lagvander3d', 'lagcompanion',
+    'laggauss', 'lagweight']
+
+lagtrim = pu.trimcoef
+
+
+def poly2lag(pol):
+    """
+    poly2lag(pol)
+
+    Convert a polynomial to a Laguerre series.
+
+    Convert an array representing the coefficients of a polynomial (relative
+    to the "standard" basis) ordered from lowest degree to highest, to an
+    array of the coefficients of the equivalent Laguerre series, ordered
+    from lowest to highest degree.
+
+    Parameters
+    ----------
+    pol : array_like
+        1-D array containing the polynomial coefficients
+
+    Returns
+    -------
+    c : ndarray
+        1-D array containing the coefficients of the equivalent Laguerre
+        series.
+
+    See Also
+    --------
+    lag2poly
+
+    Notes
+    -----
+    The easy way to do conversions between polynomial basis sets
+    is to use the convert method of a class instance.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import poly2lag
+    >>> poly2lag(np.arange(4))
+    array([ 23., -63.,  58., -18.])
+
+    """
+    [pol] = pu.as_series([pol])
+    res = 0
+    for p in pol[::-1]:
+        res = lagadd(lagmulx(res), p)
+    return res
+
+
+def lag2poly(c):
+    """
+    Convert a Laguerre series to a polynomial.
+
+    Convert an array representing the coefficients of a Laguerre series,
+    ordered from lowest degree to highest, to an array of the coefficients
+    of the equivalent polynomial (relative to the "standard" basis) ordered
+    from lowest to highest degree.
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array containing the Laguerre series coefficients, ordered
+        from lowest order term to highest.
+
+    Returns
+    -------
+    pol : ndarray
+        1-D array containing the coefficients of the equivalent polynomial
+        (relative to the "standard" basis) ordered from lowest order term
+        to highest.
+
+    See Also
+    --------
+    poly2lag
+
+    Notes
+    -----
+    The easy way to do conversions between polynomial basis sets
+    is to use the convert method of a class instance.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lag2poly
+    >>> lag2poly([ 23., -63.,  58., -18.])
+    array([0., 1., 2., 3.])
+
+    """
+    from .polynomial import polyadd, polysub, polymulx
+
+    [c] = pu.as_series([c])
+    n = len(c)
+    if n == 1:
+        return c
+    else:
+        c0 = c[-2]
+        c1 = c[-1]
+        # i is the current degree of c1
+        for i in range(n - 1, 1, -1):
+            tmp = c0
+            c0 = polysub(c[i - 2], (c1*(i - 1))/i)
+            c1 = polyadd(tmp, polysub((2*i - 1)*c1, polymulx(c1))/i)
+        return polyadd(c0, polysub(c1, polymulx(c1)))
+
+#
+# These are constant arrays are of integer type so as to be compatible
+# with the widest range of other types, such as Decimal.
+#
+
+# Laguerre
+lagdomain = np.array([0, 1])
+
+# Laguerre coefficients representing zero.
+lagzero = np.array([0])
+
+# Laguerre coefficients representing one.
+lagone = np.array([1])
+
+# Laguerre coefficients representing the identity x.
+lagx = np.array([1, -1])
+
+
+def lagline(off, scl):
+    """
+    Laguerre series whose graph is a straight line.
+
+    Parameters
+    ----------
+    off, scl : scalars
+        The specified line is given by ``off + scl*x``.
+
+    Returns
+    -------
+    y : ndarray
+        This module's representation of the Laguerre series for
+        ``off + scl*x``.
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyline
+    numpy.polynomial.chebyshev.chebline
+    numpy.polynomial.legendre.legline
+    numpy.polynomial.hermite.hermline
+    numpy.polynomial.hermite_e.hermeline
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagline, lagval
+    >>> lagval(0,lagline(3, 2))
+    3.0
+    >>> lagval(1,lagline(3, 2))
+    5.0
+
+    """
+    if scl != 0:
+        return np.array([off + scl, -scl])
+    else:
+        return np.array([off])
+
+
+def lagfromroots(roots):
+    """
+    Generate a Laguerre series with given roots.
+
+    The function returns the coefficients of the polynomial
+
+    .. math:: p(x) = (x - r_0) * (x - r_1) * ... * (x - r_n),
+
+    in Laguerre form, where the `r_n` are the roots specified in `roots`.
+    If a zero has multiplicity n, then it must appear in `roots` n times.
+    For instance, if 2 is a root of multiplicity three and 3 is a root of
+    multiplicity 2, then `roots` looks something like [2, 2, 2, 3, 3]. The
+    roots can appear in any order.
+
+    If the returned coefficients are `c`, then
+
+    .. math:: p(x) = c_0 + c_1 * L_1(x) + ... +  c_n * L_n(x)
+
+    The coefficient of the last term is not generally 1 for monic
+    polynomials in Laguerre form.
+
+    Parameters
+    ----------
+    roots : array_like
+        Sequence containing the roots.
+
+    Returns
+    -------
+    out : ndarray
+        1-D array of coefficients.  If all roots are real then `out` is a
+        real array, if some of the roots are complex, then `out` is complex
+        even if all the coefficients in the result are real (see Examples
+        below).
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyfromroots
+    numpy.polynomial.legendre.legfromroots
+    numpy.polynomial.chebyshev.chebfromroots
+    numpy.polynomial.hermite.hermfromroots
+    numpy.polynomial.hermite_e.hermefromroots
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagfromroots, lagval
+    >>> coef = lagfromroots((-1, 0, 1))
+    >>> lagval((-1, 0, 1), coef)
+    array([0.,  0.,  0.])
+    >>> coef = lagfromroots((-1j, 1j))
+    >>> lagval((-1j, 1j), coef)
+    array([0.+0.j, 0.+0.j])
+
+    """
+    return pu._fromroots(lagline, lagmul, roots)
+
+
+def lagadd(c1, c2):
+    """
+    Add one Laguerre series to another.
+
+    Returns the sum of two Laguerre series `c1` + `c2`.  The arguments
+    are sequences of coefficients ordered from lowest order term to
+    highest, i.e., [1,2,3] represents the series ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Laguerre series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Array representing the Laguerre series of their sum.
+
+    See Also
+    --------
+    lagsub, lagmulx, lagmul, lagdiv, lagpow
+
+    Notes
+    -----
+    Unlike multiplication, division, etc., the sum of two Laguerre series
+    is a Laguerre series (without having to "reproject" the result onto
+    the basis set) so addition, just like that of "standard" polynomials,
+    is simply "component-wise."
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagadd
+    >>> lagadd([1, 2, 3], [1, 2, 3, 4])
+    array([2.,  4.,  6.,  4.])
+
+
+    """
+    return pu._add(c1, c2)
+
+
+def lagsub(c1, c2):
+    """
+    Subtract one Laguerre series from another.
+
+    Returns the difference of two Laguerre series `c1` - `c2`.  The
+    sequences of coefficients are from lowest order term to highest, i.e.,
+    [1,2,3] represents the series ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Laguerre series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Of Laguerre series coefficients representing their difference.
+
+    See Also
+    --------
+    lagadd, lagmulx, lagmul, lagdiv, lagpow
+
+    Notes
+    -----
+    Unlike multiplication, division, etc., the difference of two Laguerre
+    series is a Laguerre series (without having to "reproject" the result
+    onto the basis set) so subtraction, just like that of "standard"
+    polynomials, is simply "component-wise."
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagsub
+    >>> lagsub([1, 2, 3, 4], [1, 2, 3])
+    array([0.,  0.,  0.,  4.])
+
+    """
+    return pu._sub(c1, c2)
+
+
+def lagmulx(c):
+    """Multiply a Laguerre series by x.
+
+    Multiply the Laguerre series `c` by x, where x is the independent
+    variable.
+
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Laguerre series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Array representing the result of the multiplication.
+
+    See Also
+    --------
+    lagadd, lagsub, lagmul, lagdiv, lagpow
+
+    Notes
+    -----
+    The multiplication uses the recursion relationship for Laguerre
+    polynomials in the form
+
+    .. math::
+
+        xP_i(x) = (-(i + 1)*P_{i + 1}(x) + (2i + 1)P_{i}(x) - iP_{i - 1}(x))
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagmulx
+    >>> lagmulx([1, 2, 3])
+    array([-1.,  -1.,  11.,  -9.])
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    # The zero series needs special treatment
+    if len(c) == 1 and c[0] == 0:
+        return c
+
+    prd = np.empty(len(c) + 1, dtype=c.dtype)
+    prd[0] = c[0]
+    prd[1] = -c[0]
+    for i in range(1, len(c)):
+        prd[i + 1] = -c[i]*(i + 1)
+        prd[i] += c[i]*(2*i + 1)
+        prd[i - 1] -= c[i]*i
+    return prd
+
+
+def lagmul(c1, c2):
+    """
+    Multiply one Laguerre series by another.
+
+    Returns the product of two Laguerre series `c1` * `c2`.  The arguments
+    are sequences of coefficients, from lowest order "term" to highest,
+    e.g., [1,2,3] represents the series ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Laguerre series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Of Laguerre series coefficients representing their product.
+
+    See Also
+    --------
+    lagadd, lagsub, lagmulx, lagdiv, lagpow
+
+    Notes
+    -----
+    In general, the (polynomial) product of two C-series results in terms
+    that are not in the Laguerre polynomial basis set.  Thus, to express
+    the product as a Laguerre series, it is necessary to "reproject" the
+    product onto said basis set, which may produce "unintuitive" (but
+    correct) results; see Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagmul
+    >>> lagmul([1, 2, 3], [0, 1, 2])
+    array([  8., -13.,  38., -51.,  36.])
+
+    """
+    # s1, s2 are trimmed copies
+    [c1, c2] = pu.as_series([c1, c2])
+
+    if len(c1) > len(c2):
+        c = c2
+        xs = c1
+    else:
+        c = c1
+        xs = c2
+
+    if len(c) == 1:
+        c0 = c[0]*xs
+        c1 = 0
+    elif len(c) == 2:
+        c0 = c[0]*xs
+        c1 = c[1]*xs
+    else:
+        nd = len(c)
+        c0 = c[-2]*xs
+        c1 = c[-1]*xs
+        for i in range(3, len(c) + 1):
+            tmp = c0
+            nd = nd - 1
+            c0 = lagsub(c[-i]*xs, (c1*(nd - 1))/nd)
+            c1 = lagadd(tmp, lagsub((2*nd - 1)*c1, lagmulx(c1))/nd)
+    return lagadd(c0, lagsub(c1, lagmulx(c1)))
+
+
+def lagdiv(c1, c2):
+    """
+    Divide one Laguerre series by another.
+
+    Returns the quotient-with-remainder of two Laguerre series
+    `c1` / `c2`.  The arguments are sequences of coefficients from lowest
+    order "term" to highest, e.g., [1,2,3] represents the series
+    ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Laguerre series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    [quo, rem] : ndarrays
+        Of Laguerre series coefficients representing the quotient and
+        remainder.
+
+    See Also
+    --------
+    lagadd, lagsub, lagmulx, lagmul, lagpow
+
+    Notes
+    -----
+    In general, the (polynomial) division of one Laguerre series by another
+    results in quotient and remainder terms that are not in the Laguerre
+    polynomial basis set.  Thus, to express these results as a Laguerre
+    series, it is necessary to "reproject" the results onto the Laguerre
+    basis set, which may produce "unintuitive" (but correct) results; see
+    Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagdiv
+    >>> lagdiv([  8., -13.,  38., -51.,  36.], [0, 1, 2])
+    (array([1., 2., 3.]), array([0.]))
+    >>> lagdiv([  9., -12.,  38., -51.,  36.], [0, 1, 2])
+    (array([1., 2., 3.]), array([1., 1.]))
+
+    """
+    return pu._div(lagmul, c1, c2)
+
+
+def lagpow(c, pow, maxpower=16):
+    """Raise a Laguerre series to a power.
+
+    Returns the Laguerre series `c` raised to the power `pow`. The
+    argument `c` is a sequence of coefficients ordered from low to high.
+    i.e., [1,2,3] is the series  ``P_0 + 2*P_1 + 3*P_2.``
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Laguerre series coefficients ordered from low to
+        high.
+    pow : integer
+        Power to which the series will be raised
+    maxpower : integer, optional
+        Maximum power allowed. This is mainly to limit growth of the series
+        to unmanageable size. Default is 16
+
+    Returns
+    -------
+    coef : ndarray
+        Laguerre series of power.
+
+    See Also
+    --------
+    lagadd, lagsub, lagmulx, lagmul, lagdiv
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagpow
+    >>> lagpow([1, 2, 3], 2)
+    array([ 14., -16.,  56., -72.,  54.])
+
+    """
+    return pu._pow(lagmul, c, pow, maxpower)
+
+
+def lagder(c, m=1, scl=1, axis=0):
+    """
+    Differentiate a Laguerre series.
+
+    Returns the Laguerre series coefficients `c` differentiated `m` times
+    along `axis`.  At each iteration the result is multiplied by `scl` (the
+    scaling factor is for use in a linear change of variable). The argument
+    `c` is an array of coefficients from low to high degree along each
+    axis, e.g., [1,2,3] represents the series ``1*L_0 + 2*L_1 + 3*L_2``
+    while [[1,2],[1,2]] represents ``1*L_0(x)*L_0(y) + 1*L_1(x)*L_0(y) +
+    2*L_0(x)*L_1(y) + 2*L_1(x)*L_1(y)`` if axis=0 is ``x`` and axis=1 is
+    ``y``.
+
+    Parameters
+    ----------
+    c : array_like
+        Array of Laguerre series coefficients. If `c` is multidimensional
+        the different axis correspond to different variables with the
+        degree in each axis given by the corresponding index.
+    m : int, optional
+        Number of derivatives taken, must be non-negative. (Default: 1)
+    scl : scalar, optional
+        Each differentiation is multiplied by `scl`.  The end result is
+        multiplication by ``scl**m``.  This is for use in a linear change of
+        variable. (Default: 1)
+    axis : int, optional
+        Axis over which the derivative is taken. (Default: 0).
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    der : ndarray
+        Laguerre series of the derivative.
+
+    See Also
+    --------
+    lagint
+
+    Notes
+    -----
+    In general, the result of differentiating a Laguerre series does not
+    resemble the same operation on a power series. Thus the result of this
+    function may be "unintuitive," albeit correct; see Examples section
+    below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagder
+    >>> lagder([ 1.,  1.,  1., -3.])
+    array([1.,  2.,  3.])
+    >>> lagder([ 1.,  0.,  0., -4.,  3.], m=2)
+    array([1.,  2.,  3.])
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+
+    cnt = pu._deprecate_as_int(m, "the order of derivation")
+    iaxis = pu._deprecate_as_int(axis, "the axis")
+    if cnt < 0:
+        raise ValueError("The order of derivation must be non-negative")
+    iaxis = normalize_axis_index(iaxis, c.ndim)
+
+    if cnt == 0:
+        return c
+
+    c = np.moveaxis(c, iaxis, 0)
+    n = len(c)
+    if cnt >= n:
+        c = c[:1]*0
+    else:
+        for i in range(cnt):
+            n = n - 1
+            c *= scl
+            der = np.empty((n,) + c.shape[1:], dtype=c.dtype)
+            for j in range(n, 1, -1):
+                der[j - 1] = -c[j]
+                c[j - 1] += c[j]
+            der[0] = -c[1]
+            c = der
+    c = np.moveaxis(c, 0, iaxis)
+    return c
+
+
+def lagint(c, m=1, k=[], lbnd=0, scl=1, axis=0):
+    """
+    Integrate a Laguerre series.
+
+    Returns the Laguerre series coefficients `c` integrated `m` times from
+    `lbnd` along `axis`. At each iteration the resulting series is
+    **multiplied** by `scl` and an integration constant, `k`, is added.
+    The scaling factor is for use in a linear change of variable.  ("Buyer
+    beware": note that, depending on what one is doing, one may want `scl`
+    to be the reciprocal of what one might expect; for more information,
+    see the Notes section below.)  The argument `c` is an array of
+    coefficients from low to high degree along each axis, e.g., [1,2,3]
+    represents the series ``L_0 + 2*L_1 + 3*L_2`` while [[1,2],[1,2]]
+    represents ``1*L_0(x)*L_0(y) + 1*L_1(x)*L_0(y) + 2*L_0(x)*L_1(y) +
+    2*L_1(x)*L_1(y)`` if axis=0 is ``x`` and axis=1 is ``y``.
+
+
+    Parameters
+    ----------
+    c : array_like
+        Array of Laguerre series coefficients. If `c` is multidimensional
+        the different axis correspond to different variables with the
+        degree in each axis given by the corresponding index.
+    m : int, optional
+        Order of integration, must be positive. (Default: 1)
+    k : {[], list, scalar}, optional
+        Integration constant(s).  The value of the first integral at
+        ``lbnd`` is the first value in the list, the value of the second
+        integral at ``lbnd`` is the second value, etc.  If ``k == []`` (the
+        default), all constants are set to zero.  If ``m == 1``, a single
+        scalar can be given instead of a list.
+    lbnd : scalar, optional
+        The lower bound of the integral. (Default: 0)
+    scl : scalar, optional
+        Following each integration the result is *multiplied* by `scl`
+        before the integration constant is added. (Default: 1)
+    axis : int, optional
+        Axis over which the integral is taken. (Default: 0).
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    S : ndarray
+        Laguerre series coefficients of the integral.
+
+    Raises
+    ------
+    ValueError
+        If ``m < 0``, ``len(k) > m``, ``np.ndim(lbnd) != 0``, or
+        ``np.ndim(scl) != 0``.
+
+    See Also
+    --------
+    lagder
+
+    Notes
+    -----
+    Note that the result of each integration is *multiplied* by `scl`.
+    Why is this important to note?  Say one is making a linear change of
+    variable :math:`u = ax + b` in an integral relative to `x`.  Then
+    :math:`dx = du/a`, so one will need to set `scl` equal to
+    :math:`1/a` - perhaps not what one would have first thought.
+
+    Also note that, in general, the result of integrating a C-series needs
+    to be "reprojected" onto the C-series basis set.  Thus, typically,
+    the result of this function is "unintuitive," albeit correct; see
+    Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagint
+    >>> lagint([1,2,3])
+    array([ 1.,  1.,  1., -3.])
+    >>> lagint([1,2,3], m=2)
+    array([ 1.,  0.,  0., -4.,  3.])
+    >>> lagint([1,2,3], k=1)
+    array([ 2.,  1.,  1., -3.])
+    >>> lagint([1,2,3], lbnd=-1)
+    array([11.5,  1. ,  1. , -3. ])
+    >>> lagint([1,2], m=2, k=[1,2], lbnd=-1)
+    array([ 11.16666667,  -5.        ,  -3.        ,   2.        ]) # may vary
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    if not np.iterable(k):
+        k = [k]
+    cnt = pu._deprecate_as_int(m, "the order of integration")
+    iaxis = pu._deprecate_as_int(axis, "the axis")
+    if cnt < 0:
+        raise ValueError("The order of integration must be non-negative")
+    if len(k) > cnt:
+        raise ValueError("Too many integration constants")
+    if np.ndim(lbnd) != 0:
+        raise ValueError("lbnd must be a scalar.")
+    if np.ndim(scl) != 0:
+        raise ValueError("scl must be a scalar.")
+    iaxis = normalize_axis_index(iaxis, c.ndim)
+
+    if cnt == 0:
+        return c
+
+    c = np.moveaxis(c, iaxis, 0)
+    k = list(k) + [0]*(cnt - len(k))
+    for i in range(cnt):
+        n = len(c)
+        c *= scl
+        if n == 1 and np.all(c[0] == 0):
+            c[0] += k[i]
+        else:
+            tmp = np.empty((n + 1,) + c.shape[1:], dtype=c.dtype)
+            tmp[0] = c[0]
+            tmp[1] = -c[0]
+            for j in range(1, n):
+                tmp[j] += c[j]
+                tmp[j + 1] = -c[j]
+            tmp[0] += k[i] - lagval(lbnd, tmp)
+            c = tmp
+    c = np.moveaxis(c, 0, iaxis)
+    return c
+
+
+def lagval(x, c, tensor=True):
+    """
+    Evaluate a Laguerre series at points x.
+
+    If `c` is of length `n + 1`, this function returns the value:
+
+    .. math:: p(x) = c_0 * L_0(x) + c_1 * L_1(x) + ... + c_n * L_n(x)
+
+    The parameter `x` is converted to an array only if it is a tuple or a
+    list, otherwise it is treated as a scalar. In either case, either `x`
+    or its elements must support multiplication and addition both with
+    themselves and with the elements of `c`.
+
+    If `c` is a 1-D array, then `p(x)` will have the same shape as `x`.  If
+    `c` is multidimensional, then the shape of the result depends on the
+    value of `tensor`. If `tensor` is true the shape will be c.shape[1:] +
+    x.shape. If `tensor` is false the shape will be c.shape[1:]. Note that
+    scalars have shape (,).
+
+    Trailing zeros in the coefficients will be used in the evaluation, so
+    they should be avoided if efficiency is a concern.
+
+    Parameters
+    ----------
+    x : array_like, compatible object
+        If `x` is a list or tuple, it is converted to an ndarray, otherwise
+        it is left unchanged and treated as a scalar. In either case, `x`
+        or its elements must support addition and multiplication with
+        themselves and with the elements of `c`.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree n are contained in c[n]. If `c` is multidimensional the
+        remaining indices enumerate multiple polynomials. In the two
+        dimensional case the coefficients may be thought of as stored in
+        the columns of `c`.
+    tensor : boolean, optional
+        If True, the shape of the coefficient array is extended with ones
+        on the right, one for each dimension of `x`. Scalars have dimension 0
+        for this action. The result is that every column of coefficients in
+        `c` is evaluated for every element of `x`. If False, `x` is broadcast
+        over the columns of `c` for the evaluation.  This keyword is useful
+        when `c` is multidimensional. The default value is True.
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    values : ndarray, algebra_like
+        The shape of the return value is described above.
+
+    See Also
+    --------
+    lagval2d, laggrid2d, lagval3d, laggrid3d
+
+    Notes
+    -----
+    The evaluation uses Clenshaw recursion, aka synthetic division.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagval
+    >>> coef = [1,2,3]
+    >>> lagval(1, coef)
+    -0.5
+    >>> lagval([[1,2],[3,4]], coef)
+    array([[-0.5, -4. ],
+           [-4.5, -2. ]])
+
+    """
+    c = np.array(c, ndmin=1, copy=False)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    if isinstance(x, (tuple, list)):
+        x = np.asarray(x)
+    if isinstance(x, np.ndarray) and tensor:
+        c = c.reshape(c.shape + (1,)*x.ndim)
+
+    if len(c) == 1:
+        c0 = c[0]
+        c1 = 0
+    elif len(c) == 2:
+        c0 = c[0]
+        c1 = c[1]
+    else:
+        nd = len(c)
+        c0 = c[-2]
+        c1 = c[-1]
+        for i in range(3, len(c) + 1):
+            tmp = c0
+            nd = nd - 1
+            c0 = c[-i] - (c1*(nd - 1))/nd
+            c1 = tmp + (c1*((2*nd - 1) - x))/nd
+    return c0 + c1*(1 - x)
+
+
+def lagval2d(x, y, c):
+    """
+    Evaluate a 2-D Laguerre series at points (x, y).
+
+    This function returns the values:
+
+    .. math:: p(x,y) = \\sum_{i,j} c_{i,j} * L_i(x) * L_j(y)
+
+    The parameters `x` and `y` are converted to arrays only if they are
+    tuples or a lists, otherwise they are treated as a scalars and they
+    must have the same shape after conversion. In either case, either `x`
+    and `y` or their elements must support multiplication and addition both
+    with themselves and with the elements of `c`.
+
+    If `c` is a 1-D array a one is implicitly appended to its shape to make
+    it 2-D. The shape of the result will be c.shape[2:] + x.shape.
+
+    Parameters
+    ----------
+    x, y : array_like, compatible objects
+        The two dimensional series is evaluated at the points `(x, y)`,
+        where `x` and `y` must have the same shape. If `x` or `y` is a list
+        or tuple, it is first converted to an ndarray, otherwise it is left
+        unchanged and if it isn't an ndarray it is treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term
+        of multi-degree i,j is contained in ``c[i,j]``. If `c` has
+        dimension greater than two the remaining indices enumerate multiple
+        sets of coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points formed with
+        pairs of corresponding values from `x` and `y`.
+
+    See Also
+    --------
+    lagval, laggrid2d, lagval3d, laggrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._valnd(lagval, c, x, y)
+
+
+def laggrid2d(x, y, c):
+    """
+    Evaluate a 2-D Laguerre series on the Cartesian product of x and y.
+
+    This function returns the values:
+
+    .. math:: p(a,b) = \\sum_{i,j} c_{i,j} * L_i(a) * L_j(b)
+
+    where the points `(a, b)` consist of all pairs formed by taking
+    `a` from `x` and `b` from `y`. The resulting points form a grid with
+    `x` in the first dimension and `y` in the second.
+
+    The parameters `x` and `y` are converted to arrays only if they are
+    tuples or a lists, otherwise they are treated as a scalars. In either
+    case, either `x` and `y` or their elements must support multiplication
+    and addition both with themselves and with the elements of `c`.
+
+    If `c` has fewer than two dimensions, ones are implicitly appended to
+    its shape to make it 2-D. The shape of the result will be c.shape[2:] +
+    x.shape + y.shape.
+
+    Parameters
+    ----------
+    x, y : array_like, compatible objects
+        The two dimensional series is evaluated at the points in the
+        Cartesian product of `x` and `y`.  If `x` or `y` is a list or
+        tuple, it is first converted to an ndarray, otherwise it is left
+        unchanged and, if it isn't an ndarray, it is treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term of
+        multi-degree i,j is contained in `c[i,j]`. If `c` has dimension
+        greater than two the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional Chebyshev series at points in the
+        Cartesian product of `x` and `y`.
+
+    See Also
+    --------
+    lagval, lagval2d, lagval3d, laggrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._gridnd(lagval, c, x, y)
+
+
+def lagval3d(x, y, z, c):
+    """
+    Evaluate a 3-D Laguerre series at points (x, y, z).
+
+    This function returns the values:
+
+    .. math:: p(x,y,z) = \\sum_{i,j,k} c_{i,j,k} * L_i(x) * L_j(y) * L_k(z)
+
+    The parameters `x`, `y`, and `z` are converted to arrays only if
+    they are tuples or a lists, otherwise they are treated as a scalars and
+    they must have the same shape after conversion. In either case, either
+    `x`, `y`, and `z` or their elements must support multiplication and
+    addition both with themselves and with the elements of `c`.
+
+    If `c` has fewer than 3 dimensions, ones are implicitly appended to its
+    shape to make it 3-D. The shape of the result will be c.shape[3:] +
+    x.shape.
+
+    Parameters
+    ----------
+    x, y, z : array_like, compatible object
+        The three dimensional series is evaluated at the points
+        `(x, y, z)`, where `x`, `y`, and `z` must have the same shape.  If
+        any of `x`, `y`, or `z` is a list or tuple, it is first converted
+        to an ndarray, otherwise it is left unchanged and if it isn't an
+        ndarray it is  treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term of
+        multi-degree i,j,k is contained in ``c[i,j,k]``. If `c` has dimension
+        greater than 3 the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the multidimensional polynomial on points formed with
+        triples of corresponding values from `x`, `y`, and `z`.
+
+    See Also
+    --------
+    lagval, lagval2d, laggrid2d, laggrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._valnd(lagval, c, x, y, z)
+
+
+def laggrid3d(x, y, z, c):
+    """
+    Evaluate a 3-D Laguerre series on the Cartesian product of x, y, and z.
+
+    This function returns the values:
+
+    .. math:: p(a,b,c) = \\sum_{i,j,k} c_{i,j,k} * L_i(a) * L_j(b) * L_k(c)
+
+    where the points `(a, b, c)` consist of all triples formed by taking
+    `a` from `x`, `b` from `y`, and `c` from `z`. The resulting points form
+    a grid with `x` in the first dimension, `y` in the second, and `z` in
+    the third.
+
+    The parameters `x`, `y`, and `z` are converted to arrays only if they
+    are tuples or a lists, otherwise they are treated as a scalars. In
+    either case, either `x`, `y`, and `z` or their elements must support
+    multiplication and addition both with themselves and with the elements
+    of `c`.
+
+    If `c` has fewer than three dimensions, ones are implicitly appended to
+    its shape to make it 3-D. The shape of the result will be c.shape[3:] +
+    x.shape + y.shape + z.shape.
+
+    Parameters
+    ----------
+    x, y, z : array_like, compatible objects
+        The three dimensional series is evaluated at the points in the
+        Cartesian product of `x`, `y`, and `z`.  If `x`,`y`, or `z` is a
+        list or tuple, it is first converted to an ndarray, otherwise it is
+        left unchanged and, if it isn't an ndarray, it is treated as a
+        scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree i,j are contained in ``c[i,j]``. If `c` has dimension
+        greater than two the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points in the Cartesian
+        product of `x` and `y`.
+
+    See Also
+    --------
+    lagval, lagval2d, laggrid2d, lagval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._gridnd(lagval, c, x, y, z)
+
+
+def lagvander(x, deg):
+    """Pseudo-Vandermonde matrix of given degree.
+
+    Returns the pseudo-Vandermonde matrix of degree `deg` and sample points
+    `x`. The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., i] = L_i(x)
+
+    where `0 <= i <= deg`. The leading indices of `V` index the elements of
+    `x` and the last index is the degree of the Laguerre polynomial.
+
+    If `c` is a 1-D array of coefficients of length `n + 1` and `V` is the
+    array ``V = lagvander(x, n)``, then ``np.dot(V, c)`` and
+    ``lagval(x, c)`` are the same up to roundoff. This equivalence is
+    useful both for least squares fitting and for the evaluation of a large
+    number of Laguerre series of the same degree and sample points.
+
+    Parameters
+    ----------
+    x : array_like
+        Array of points. The dtype is converted to float64 or complex128
+        depending on whether any of the elements are complex. If `x` is
+        scalar it is converted to a 1-D array.
+    deg : int
+        Degree of the resulting matrix.
+
+    Returns
+    -------
+    vander : ndarray
+        The pseudo-Vandermonde matrix. The shape of the returned matrix is
+        ``x.shape + (deg + 1,)``, where The last index is the degree of the
+        corresponding Laguerre polynomial.  The dtype will be the same as
+        the converted `x`.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagvander
+    >>> x = np.array([0, 1, 2])
+    >>> lagvander(x, 3)
+    array([[ 1.        ,  1.        ,  1.        ,  1.        ],
+           [ 1.        ,  0.        , -0.5       , -0.66666667],
+           [ 1.        , -1.        , -1.        , -0.33333333]])
+
+    """
+    ideg = pu._deprecate_as_int(deg, "deg")
+    if ideg < 0:
+        raise ValueError("deg must be non-negative")
+
+    x = np.array(x, copy=False, ndmin=1) + 0.0
+    dims = (ideg + 1,) + x.shape
+    dtyp = x.dtype
+    v = np.empty(dims, dtype=dtyp)
+    v[0] = x*0 + 1
+    if ideg > 0:
+        v[1] = 1 - x
+        for i in range(2, ideg + 1):
+            v[i] = (v[i-1]*(2*i - 1 - x) - v[i-2]*(i - 1))/i
+    return np.moveaxis(v, 0, -1)
+
+
+def lagvander2d(x, y, deg):
+    """Pseudo-Vandermonde matrix of given degrees.
+
+    Returns the pseudo-Vandermonde matrix of degrees `deg` and sample
+    points `(x, y)`. The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., (deg[1] + 1)*i + j] = L_i(x) * L_j(y),
+
+    where `0 <= i <= deg[0]` and `0 <= j <= deg[1]`. The leading indices of
+    `V` index the points `(x, y)` and the last index encodes the degrees of
+    the Laguerre polynomials.
+
+    If ``V = lagvander2d(x, y, [xdeg, ydeg])``, then the columns of `V`
+    correspond to the elements of a 2-D coefficient array `c` of shape
+    (xdeg + 1, ydeg + 1) in the order
+
+    .. math:: c_{00}, c_{01}, c_{02} ... , c_{10}, c_{11}, c_{12} ...
+
+    and ``np.dot(V, c.flat)`` and ``lagval2d(x, y, c)`` will be the same
+    up to roundoff. This equivalence is useful both for least squares
+    fitting and for the evaluation of a large number of 2-D Laguerre
+    series of the same degrees and sample points.
+
+    Parameters
+    ----------
+    x, y : array_like
+        Arrays of point coordinates, all of the same shape. The dtypes
+        will be converted to either float64 or complex128 depending on
+        whether any of the elements are complex. Scalars are converted to
+        1-D arrays.
+    deg : list of ints
+        List of maximum degrees of the form [x_deg, y_deg].
+
+    Returns
+    -------
+    vander2d : ndarray
+        The shape of the returned matrix is ``x.shape + (order,)``, where
+        :math:`order = (deg[0]+1)*(deg[1]+1)`.  The dtype will be the same
+        as the converted `x` and `y`.
+
+    See Also
+    --------
+    lagvander, lagvander3d, lagval2d, lagval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._vander_nd_flat((lagvander, lagvander), (x, y), deg)
+
+
+def lagvander3d(x, y, z, deg):
+    """Pseudo-Vandermonde matrix of given degrees.
+
+    Returns the pseudo-Vandermonde matrix of degrees `deg` and sample
+    points `(x, y, z)`. If `l, m, n` are the given degrees in `x, y, z`,
+    then The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., (m+1)(n+1)i + (n+1)j + k] = L_i(x)*L_j(y)*L_k(z),
+
+    where `0 <= i <= l`, `0 <= j <= m`, and `0 <= j <= n`.  The leading
+    indices of `V` index the points `(x, y, z)` and the last index encodes
+    the degrees of the Laguerre polynomials.
+
+    If ``V = lagvander3d(x, y, z, [xdeg, ydeg, zdeg])``, then the columns
+    of `V` correspond to the elements of a 3-D coefficient array `c` of
+    shape (xdeg + 1, ydeg + 1, zdeg + 1) in the order
+
+    .. math:: c_{000}, c_{001}, c_{002},... , c_{010}, c_{011}, c_{012},...
+
+    and  ``np.dot(V, c.flat)`` and ``lagval3d(x, y, z, c)`` will be the
+    same up to roundoff. This equivalence is useful both for least squares
+    fitting and for the evaluation of a large number of 3-D Laguerre
+    series of the same degrees and sample points.
+
+    Parameters
+    ----------
+    x, y, z : array_like
+        Arrays of point coordinates, all of the same shape. The dtypes will
+        be converted to either float64 or complex128 depending on whether
+        any of the elements are complex. Scalars are converted to 1-D
+        arrays.
+    deg : list of ints
+        List of maximum degrees of the form [x_deg, y_deg, z_deg].
+
+    Returns
+    -------
+    vander3d : ndarray
+        The shape of the returned matrix is ``x.shape + (order,)``, where
+        :math:`order = (deg[0]+1)*(deg[1]+1)*(deg[2]+1)`.  The dtype will
+        be the same as the converted `x`, `y`, and `z`.
+
+    See Also
+    --------
+    lagvander, lagvander3d, lagval2d, lagval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._vander_nd_flat((lagvander, lagvander, lagvander), (x, y, z), deg)
+
+
+def lagfit(x, y, deg, rcond=None, full=False, w=None):
+    """
+    Least squares fit of Laguerre series to data.
+
+    Return the coefficients of a Laguerre series of degree `deg` that is the
+    least squares fit to the data values `y` given at points `x`. If `y` is
+    1-D the returned coefficients will also be 1-D. If `y` is 2-D multiple
+    fits are done, one for each column of `y`, and the resulting
+    coefficients are stored in the corresponding columns of a 2-D return.
+    The fitted polynomial(s) are in the form
+
+    .. math::  p(x) = c_0 + c_1 * L_1(x) + ... + c_n * L_n(x),
+
+    where ``n`` is `deg`.
+
+    Parameters
+    ----------
+    x : array_like, shape (M,)
+        x-coordinates of the M sample points ``(x[i], y[i])``.
+    y : array_like, shape (M,) or (M, K)
+        y-coordinates of the sample points. Several data sets of sample
+        points sharing the same x-coordinates can be fitted at once by
+        passing in a 2D-array that contains one dataset per column.
+    deg : int or 1-D array_like
+        Degree(s) of the fitting polynomials. If `deg` is a single integer
+        all terms up to and including the `deg`'th term are included in the
+        fit. For NumPy versions >= 1.11.0 a list of integers specifying the
+        degrees of the terms to include may be used instead.
+    rcond : float, optional
+        Relative condition number of the fit. Singular values smaller than
+        this relative to the largest singular value will be ignored. The
+        default value is len(x)*eps, where eps is the relative precision of
+        the float type, about 2e-16 in most cases.
+    full : bool, optional
+        Switch determining nature of return value. When it is False (the
+        default) just the coefficients are returned, when True diagnostic
+        information from the singular value decomposition is also returned.
+    w : array_like, shape (`M`,), optional
+        Weights. If not None, the weight ``w[i]`` applies to the unsquared
+        residual ``y[i] - y_hat[i]`` at ``x[i]``. Ideally the weights are
+        chosen so that the errors of the products ``w[i]*y[i]`` all have the
+        same variance.  When using inverse-variance weighting, use
+        ``w[i] = 1/sigma(y[i])``.  The default value is None.
+
+    Returns
+    -------
+    coef : ndarray, shape (M,) or (M, K)
+        Laguerre coefficients ordered from low to high. If `y` was 2-D,
+        the coefficients for the data in column *k*  of `y` are in column
+        *k*.
+
+    [residuals, rank, singular_values, rcond] : list
+        These values are only returned if ``full == True``
+
+        - residuals -- sum of squared residuals of the least squares fit
+        - rank -- the numerical rank of the scaled Vandermonde matrix
+        - singular_values -- singular values of the scaled Vandermonde matrix
+        - rcond -- value of `rcond`.
+
+        For more details, see `numpy.linalg.lstsq`.
+
+    Warns
+    -----
+    RankWarning
+        The rank of the coefficient matrix in the least-squares fit is
+        deficient. The warning is only raised if ``full == False``.  The
+        warnings can be turned off by
+
+        >>> import warnings
+        >>> warnings.simplefilter('ignore', np.RankWarning)
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyfit
+    numpy.polynomial.legendre.legfit
+    numpy.polynomial.chebyshev.chebfit
+    numpy.polynomial.hermite.hermfit
+    numpy.polynomial.hermite_e.hermefit
+    lagval : Evaluates a Laguerre series.
+    lagvander : pseudo Vandermonde matrix of Laguerre series.
+    lagweight : Laguerre weight function.
+    numpy.linalg.lstsq : Computes a least-squares fit from the matrix.
+    scipy.interpolate.UnivariateSpline : Computes spline fits.
+
+    Notes
+    -----
+    The solution is the coefficients of the Laguerre series ``p`` that
+    minimizes the sum of the weighted squared errors
+
+    .. math:: E = \\sum_j w_j^2 * |y_j - p(x_j)|^2,
+
+    where the :math:`w_j` are the weights. This problem is solved by
+    setting up as the (typically) overdetermined matrix equation
+
+    .. math:: V(x) * c = w * y,
+
+    where ``V`` is the weighted pseudo Vandermonde matrix of `x`, ``c`` are the
+    coefficients to be solved for, `w` are the weights, and `y` are the
+    observed values.  This equation is then solved using the singular value
+    decomposition of ``V``.
+
+    If some of the singular values of `V` are so small that they are
+    neglected, then a `RankWarning` will be issued. This means that the
+    coefficient values may be poorly determined. Using a lower order fit
+    will usually get rid of the warning.  The `rcond` parameter can also be
+    set to a value smaller than its default, but the resulting fit may be
+    spurious and have large contributions from roundoff error.
+
+    Fits using Laguerre series are probably most useful when the data can
+    be approximated by ``sqrt(w(x)) * p(x)``, where ``w(x)`` is the Laguerre
+    weight. In that case the weight ``sqrt(w(x[i]))`` should be used
+    together with data values ``y[i]/sqrt(w(x[i]))``. The weight function is
+    available as `lagweight`.
+
+    References
+    ----------
+    .. [1] Wikipedia, "Curve fitting",
+           https://en.wikipedia.org/wiki/Curve_fitting
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagfit, lagval
+    >>> x = np.linspace(0, 10)
+    >>> err = np.random.randn(len(x))/10
+    >>> y = lagval(x, [1, 2, 3]) + err
+    >>> lagfit(x, y, 2)
+    array([ 0.96971004,  2.00193749,  3.00288744]) # may vary
+
+    """
+    return pu._fit(lagvander, x, y, deg, rcond, full, w)
+
+
+def lagcompanion(c):
+    """
+    Return the companion matrix of c.
+
+    The usual companion matrix of the Laguerre polynomials is already
+    symmetric when `c` is a basis Laguerre polynomial, so no scaling is
+    applied.
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Laguerre series coefficients ordered from low to high
+        degree.
+
+    Returns
+    -------
+    mat : ndarray
+        Companion matrix of dimensions (deg, deg).
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    if len(c) < 2:
+        raise ValueError('Series must have maximum degree of at least 1.')
+    if len(c) == 2:
+        return np.array([[1 + c[0]/c[1]]])
+
+    n = len(c) - 1
+    mat = np.zeros((n, n), dtype=c.dtype)
+    top = mat.reshape(-1)[1::n+1]
+    mid = mat.reshape(-1)[0::n+1]
+    bot = mat.reshape(-1)[n::n+1]
+    top[...] = -np.arange(1, n)
+    mid[...] = 2.*np.arange(n) + 1.
+    bot[...] = top
+    mat[:, -1] += (c[:-1]/c[-1])*n
+    return mat
+
+
+def lagroots(c):
+    """
+    Compute the roots of a Laguerre series.
+
+    Return the roots (a.k.a. "zeros") of the polynomial
+
+    .. math:: p(x) = \\sum_i c[i] * L_i(x).
+
+    Parameters
+    ----------
+    c : 1-D array_like
+        1-D array of coefficients.
+
+    Returns
+    -------
+    out : ndarray
+        Array of the roots of the series. If all the roots are real,
+        then `out` is also real, otherwise it is complex.
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyroots
+    numpy.polynomial.legendre.legroots
+    numpy.polynomial.chebyshev.chebroots
+    numpy.polynomial.hermite.hermroots
+    numpy.polynomial.hermite_e.hermeroots
+
+    Notes
+    -----
+    The root estimates are obtained as the eigenvalues of the companion
+    matrix, Roots far from the origin of the complex plane may have large
+    errors due to the numerical instability of the series for such
+    values. Roots with multiplicity greater than 1 will also show larger
+    errors as the value of the series near such points is relatively
+    insensitive to errors in the roots. Isolated roots near the origin can
+    be improved by a few iterations of Newton's method.
+
+    The Laguerre series basis polynomials aren't powers of `x` so the
+    results of this function may seem unintuitive.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.laguerre import lagroots, lagfromroots
+    >>> coef = lagfromroots([0, 1, 2])
+    >>> coef
+    array([  2.,  -8.,  12.,  -6.])
+    >>> lagroots(coef)
+    array([-4.4408921e-16,  1.0000000e+00,  2.0000000e+00])
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    if len(c) <= 1:
+        return np.array([], dtype=c.dtype)
+    if len(c) == 2:
+        return np.array([1 + c[0]/c[1]])
+
+    # rotated companion matrix reduces error
+    m = lagcompanion(c)[::-1,::-1]
+    r = la.eigvals(m)
+    r.sort()
+    return r
+
+
+def laggauss(deg):
+    """
+    Gauss-Laguerre quadrature.
+
+    Computes the sample points and weights for Gauss-Laguerre quadrature.
+    These sample points and weights will correctly integrate polynomials of
+    degree :math:`2*deg - 1` or less over the interval :math:`[0, \\inf]`
+    with the weight function :math:`f(x) = \\exp(-x)`.
+
+    Parameters
+    ----------
+    deg : int
+        Number of sample points and weights. It must be >= 1.
+
+    Returns
+    -------
+    x : ndarray
+        1-D ndarray containing the sample points.
+    y : ndarray
+        1-D ndarray containing the weights.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    The results have only been tested up to degree 100 higher degrees may
+    be problematic. The weights are determined by using the fact that
+
+    .. math:: w_k = c / (L'_n(x_k) * L_{n-1}(x_k))
+
+    where :math:`c` is a constant independent of :math:`k` and :math:`x_k`
+    is the k'th root of :math:`L_n`, and then scaling the results to get
+    the right value when integrating 1.
+
+    """
+    ideg = pu._deprecate_as_int(deg, "deg")
+    if ideg <= 0:
+        raise ValueError("deg must be a positive integer")
+
+    # first approximation of roots. We use the fact that the companion
+    # matrix is symmetric in this case in order to obtain better zeros.
+    c = np.array([0]*deg + [1])
+    m = lagcompanion(c)
+    x = la.eigvalsh(m)
+
+    # improve roots by one application of Newton
+    dy = lagval(x, c)
+    df = lagval(x, lagder(c))
+    x -= dy/df
+
+    # compute the weights. We scale the factor to avoid possible numerical
+    # overflow.
+    fm = lagval(x, c[1:])
+    fm /= np.abs(fm).max()
+    df /= np.abs(df).max()
+    w = 1/(fm * df)
+
+    # scale w to get the right value, 1 in this case
+    w /= w.sum()
+
+    return x, w
+
+
+def lagweight(x):
+    """Weight function of the Laguerre polynomials.
+
+    The weight function is :math:`exp(-x)` and the interval of integration
+    is :math:`[0, \\inf]`. The Laguerre polynomials are orthogonal, but not
+    normalized, with respect to this weight function.
+
+    Parameters
+    ----------
+    x : array_like
+       Values at which the weight function will be computed.
+
+    Returns
+    -------
+    w : ndarray
+       The weight function at `x`.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    w = np.exp(-x)
+    return w
+
+#
+# Laguerre series class
+#
+
+class Laguerre(ABCPolyBase):
+    """A Laguerre series class.
+
+    The Laguerre class provides the standard Python numerical methods
+    '+', '-', '*', '//', '%', 'divmod', '**', and '()' as well as the
+    attributes and methods listed in the `ABCPolyBase` documentation.
+
+    Parameters
+    ----------
+    coef : array_like
+        Laguerre coefficients in order of increasing degree, i.e,
+        ``(1, 2, 3)`` gives ``1*L_0(x) + 2*L_1(X) + 3*L_2(x)``.
+    domain : (2,) array_like, optional
+        Domain to use. The interval ``[domain[0], domain[1]]`` is mapped
+        to the interval ``[window[0], window[1]]`` by shifting and scaling.
+        The default value is [0, 1].
+    window : (2,) array_like, optional
+        Window, see `domain` for its use. The default value is [0, 1].
+
+        .. versionadded:: 1.6.0
+    symbol : str, optional
+        Symbol used to represent the independent variable in string
+        representations of the polynomial expression, e.g. for printing.
+        The symbol must be a valid Python identifier. Default value is 'x'.
+
+        .. versionadded:: 1.24
+
+    """
+    # Virtual Functions
+    _add = staticmethod(lagadd)
+    _sub = staticmethod(lagsub)
+    _mul = staticmethod(lagmul)
+    _div = staticmethod(lagdiv)
+    _pow = staticmethod(lagpow)
+    _val = staticmethod(lagval)
+    _int = staticmethod(lagint)
+    _der = staticmethod(lagder)
+    _fit = staticmethod(lagfit)
+    _line = staticmethod(lagline)
+    _roots = staticmethod(lagroots)
+    _fromroots = staticmethod(lagfromroots)
+
+    # Virtual properties
+    domain = np.array(lagdomain)
+    window = np.array(lagdomain)
+    basis_name = 'L'
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/laguerre.pyi b/.venv/lib/python3.12/site-packages/numpy/polynomial/laguerre.pyi
new file mode 100644
index 00000000..e546bc20
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/laguerre.pyi
@@ -0,0 +1,46 @@
+from typing import Any
+
+from numpy import ndarray, dtype, int_
+from numpy.polynomial._polybase import ABCPolyBase
+from numpy.polynomial.polyutils import trimcoef
+
+__all__: list[str]
+
+lagtrim = trimcoef
+
+def poly2lag(pol): ...
+def lag2poly(c): ...
+
+lagdomain: ndarray[Any, dtype[int_]]
+lagzero: ndarray[Any, dtype[int_]]
+lagone: ndarray[Any, dtype[int_]]
+lagx: ndarray[Any, dtype[int_]]
+
+def lagline(off, scl): ...
+def lagfromroots(roots): ...
+def lagadd(c1, c2): ...
+def lagsub(c1, c2): ...
+def lagmulx(c): ...
+def lagmul(c1, c2): ...
+def lagdiv(c1, c2): ...
+def lagpow(c, pow, maxpower=...): ...
+def lagder(c, m=..., scl=..., axis=...): ...
+def lagint(c, m=..., k = ..., lbnd=..., scl=..., axis=...): ...
+def lagval(x, c, tensor=...): ...
+def lagval2d(x, y, c): ...
+def laggrid2d(x, y, c): ...
+def lagval3d(x, y, z, c): ...
+def laggrid3d(x, y, z, c): ...
+def lagvander(x, deg): ...
+def lagvander2d(x, y, deg): ...
+def lagvander3d(x, y, z, deg): ...
+def lagfit(x, y, deg, rcond=..., full=..., w=...): ...
+def lagcompanion(c): ...
+def lagroots(c): ...
+def laggauss(deg): ...
+def lagweight(x): ...
+
+class Laguerre(ABCPolyBase):
+    domain: Any
+    window: Any
+    basis_name: Any
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/legendre.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/legendre.py
new file mode 100644
index 00000000..8e9c19d9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/legendre.py
@@ -0,0 +1,1664 @@
+"""
+==================================================
+Legendre Series (:mod:`numpy.polynomial.legendre`)
+==================================================
+
+This module provides a number of objects (mostly functions) useful for
+dealing with Legendre series, including a `Legendre` class that
+encapsulates the usual arithmetic operations.  (General information
+on how this module represents and works with such polynomials is in the
+docstring for its "parent" sub-package, `numpy.polynomial`).
+
+Classes
+-------
+.. autosummary::
+   :toctree: generated/
+
+    Legendre
+
+Constants
+---------
+
+.. autosummary::
+   :toctree: generated/
+
+   legdomain
+   legzero
+   legone
+   legx
+
+Arithmetic
+----------
+
+.. autosummary::
+   :toctree: generated/
+
+   legadd
+   legsub
+   legmulx
+   legmul
+   legdiv
+   legpow
+   legval
+   legval2d
+   legval3d
+   leggrid2d
+   leggrid3d
+
+Calculus
+--------
+
+.. autosummary::
+   :toctree: generated/
+
+   legder
+   legint
+
+Misc Functions
+--------------
+
+.. autosummary::
+   :toctree: generated/
+
+   legfromroots
+   legroots
+   legvander
+   legvander2d
+   legvander3d
+   leggauss
+   legweight
+   legcompanion
+   legfit
+   legtrim
+   legline
+   leg2poly
+   poly2leg
+
+See also
+--------
+numpy.polynomial
+
+"""
+import numpy as np
+import numpy.linalg as la
+from numpy.core.multiarray import normalize_axis_index
+
+from . import polyutils as pu
+from ._polybase import ABCPolyBase
+
+__all__ = [
+    'legzero', 'legone', 'legx', 'legdomain', 'legline', 'legadd',
+    'legsub', 'legmulx', 'legmul', 'legdiv', 'legpow', 'legval', 'legder',
+    'legint', 'leg2poly', 'poly2leg', 'legfromroots', 'legvander',
+    'legfit', 'legtrim', 'legroots', 'Legendre', 'legval2d', 'legval3d',
+    'leggrid2d', 'leggrid3d', 'legvander2d', 'legvander3d', 'legcompanion',
+    'leggauss', 'legweight']
+
+legtrim = pu.trimcoef
+
+
+def poly2leg(pol):
+    """
+    Convert a polynomial to a Legendre series.
+
+    Convert an array representing the coefficients of a polynomial (relative
+    to the "standard" basis) ordered from lowest degree to highest, to an
+    array of the coefficients of the equivalent Legendre series, ordered
+    from lowest to highest degree.
+
+    Parameters
+    ----------
+    pol : array_like
+        1-D array containing the polynomial coefficients
+
+    Returns
+    -------
+    c : ndarray
+        1-D array containing the coefficients of the equivalent Legendre
+        series.
+
+    See Also
+    --------
+    leg2poly
+
+    Notes
+    -----
+    The easy way to do conversions between polynomial basis sets
+    is to use the convert method of a class instance.
+
+    Examples
+    --------
+    >>> from numpy import polynomial as P
+    >>> p = P.Polynomial(np.arange(4))
+    >>> p
+    Polynomial([0.,  1.,  2.,  3.], domain=[-1,  1], window=[-1,  1])
+    >>> c = P.Legendre(P.legendre.poly2leg(p.coef))
+    >>> c
+    Legendre([ 1.  ,  3.25,  1.  ,  0.75], domain=[-1,  1], window=[-1,  1]) # may vary
+
+    """
+    [pol] = pu.as_series([pol])
+    deg = len(pol) - 1
+    res = 0
+    for i in range(deg, -1, -1):
+        res = legadd(legmulx(res), pol[i])
+    return res
+
+
+def leg2poly(c):
+    """
+    Convert a Legendre series to a polynomial.
+
+    Convert an array representing the coefficients of a Legendre series,
+    ordered from lowest degree to highest, to an array of the coefficients
+    of the equivalent polynomial (relative to the "standard" basis) ordered
+    from lowest to highest degree.
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array containing the Legendre series coefficients, ordered
+        from lowest order term to highest.
+
+    Returns
+    -------
+    pol : ndarray
+        1-D array containing the coefficients of the equivalent polynomial
+        (relative to the "standard" basis) ordered from lowest order term
+        to highest.
+
+    See Also
+    --------
+    poly2leg
+
+    Notes
+    -----
+    The easy way to do conversions between polynomial basis sets
+    is to use the convert method of a class instance.
+
+    Examples
+    --------
+    >>> from numpy import polynomial as P
+    >>> c = P.Legendre(range(4))
+    >>> c
+    Legendre([0., 1., 2., 3.], domain=[-1,  1], window=[-1,  1])
+    >>> p = c.convert(kind=P.Polynomial)
+    >>> p
+    Polynomial([-1. , -3.5,  3. ,  7.5], domain=[-1.,  1.], window=[-1.,  1.])
+    >>> P.legendre.leg2poly(range(4))
+    array([-1. , -3.5,  3. ,  7.5])
+
+
+    """
+    from .polynomial import polyadd, polysub, polymulx
+
+    [c] = pu.as_series([c])
+    n = len(c)
+    if n < 3:
+        return c
+    else:
+        c0 = c[-2]
+        c1 = c[-1]
+        # i is the current degree of c1
+        for i in range(n - 1, 1, -1):
+            tmp = c0
+            c0 = polysub(c[i - 2], (c1*(i - 1))/i)
+            c1 = polyadd(tmp, (polymulx(c1)*(2*i - 1))/i)
+        return polyadd(c0, polymulx(c1))
+
+#
+# These are constant arrays are of integer type so as to be compatible
+# with the widest range of other types, such as Decimal.
+#
+
+# Legendre
+legdomain = np.array([-1, 1])
+
+# Legendre coefficients representing zero.
+legzero = np.array([0])
+
+# Legendre coefficients representing one.
+legone = np.array([1])
+
+# Legendre coefficients representing the identity x.
+legx = np.array([0, 1])
+
+
+def legline(off, scl):
+    """
+    Legendre series whose graph is a straight line.
+
+
+
+    Parameters
+    ----------
+    off, scl : scalars
+        The specified line is given by ``off + scl*x``.
+
+    Returns
+    -------
+    y : ndarray
+        This module's representation of the Legendre series for
+        ``off + scl*x``.
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyline
+    numpy.polynomial.chebyshev.chebline
+    numpy.polynomial.laguerre.lagline
+    numpy.polynomial.hermite.hermline
+    numpy.polynomial.hermite_e.hermeline
+
+    Examples
+    --------
+    >>> import numpy.polynomial.legendre as L
+    >>> L.legline(3,2)
+    array([3, 2])
+    >>> L.legval(-3, L.legline(3,2)) # should be -3
+    -3.0
+
+    """
+    if scl != 0:
+        return np.array([off, scl])
+    else:
+        return np.array([off])
+
+
+def legfromroots(roots):
+    """
+    Generate a Legendre series with given roots.
+
+    The function returns the coefficients of the polynomial
+
+    .. math:: p(x) = (x - r_0) * (x - r_1) * ... * (x - r_n),
+
+    in Legendre form, where the `r_n` are the roots specified in `roots`.
+    If a zero has multiplicity n, then it must appear in `roots` n times.
+    For instance, if 2 is a root of multiplicity three and 3 is a root of
+    multiplicity 2, then `roots` looks something like [2, 2, 2, 3, 3]. The
+    roots can appear in any order.
+
+    If the returned coefficients are `c`, then
+
+    .. math:: p(x) = c_0 + c_1 * L_1(x) + ... +  c_n * L_n(x)
+
+    The coefficient of the last term is not generally 1 for monic
+    polynomials in Legendre form.
+
+    Parameters
+    ----------
+    roots : array_like
+        Sequence containing the roots.
+
+    Returns
+    -------
+    out : ndarray
+        1-D array of coefficients.  If all roots are real then `out` is a
+        real array, if some of the roots are complex, then `out` is complex
+        even if all the coefficients in the result are real (see Examples
+        below).
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyfromroots
+    numpy.polynomial.chebyshev.chebfromroots
+    numpy.polynomial.laguerre.lagfromroots
+    numpy.polynomial.hermite.hermfromroots
+    numpy.polynomial.hermite_e.hermefromroots
+
+    Examples
+    --------
+    >>> import numpy.polynomial.legendre as L
+    >>> L.legfromroots((-1,0,1)) # x^3 - x relative to the standard basis
+    array([ 0. , -0.4,  0. ,  0.4])
+    >>> j = complex(0,1)
+    >>> L.legfromroots((-j,j)) # x^2 + 1 relative to the standard basis
+    array([ 1.33333333+0.j,  0.00000000+0.j,  0.66666667+0.j]) # may vary
+
+    """
+    return pu._fromroots(legline, legmul, roots)
+
+
+def legadd(c1, c2):
+    """
+    Add one Legendre series to another.
+
+    Returns the sum of two Legendre series `c1` + `c2`.  The arguments
+    are sequences of coefficients ordered from lowest order term to
+    highest, i.e., [1,2,3] represents the series ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Legendre series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Array representing the Legendre series of their sum.
+
+    See Also
+    --------
+    legsub, legmulx, legmul, legdiv, legpow
+
+    Notes
+    -----
+    Unlike multiplication, division, etc., the sum of two Legendre series
+    is a Legendre series (without having to "reproject" the result onto
+    the basis set) so addition, just like that of "standard" polynomials,
+    is simply "component-wise."
+
+    Examples
+    --------
+    >>> from numpy.polynomial import legendre as L
+    >>> c1 = (1,2,3)
+    >>> c2 = (3,2,1)
+    >>> L.legadd(c1,c2)
+    array([4.,  4.,  4.])
+
+    """
+    return pu._add(c1, c2)
+
+
+def legsub(c1, c2):
+    """
+    Subtract one Legendre series from another.
+
+    Returns the difference of two Legendre series `c1` - `c2`.  The
+    sequences of coefficients are from lowest order term to highest, i.e.,
+    [1,2,3] represents the series ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Legendre series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Of Legendre series coefficients representing their difference.
+
+    See Also
+    --------
+    legadd, legmulx, legmul, legdiv, legpow
+
+    Notes
+    -----
+    Unlike multiplication, division, etc., the difference of two Legendre
+    series is a Legendre series (without having to "reproject" the result
+    onto the basis set) so subtraction, just like that of "standard"
+    polynomials, is simply "component-wise."
+
+    Examples
+    --------
+    >>> from numpy.polynomial import legendre as L
+    >>> c1 = (1,2,3)
+    >>> c2 = (3,2,1)
+    >>> L.legsub(c1,c2)
+    array([-2.,  0.,  2.])
+    >>> L.legsub(c2,c1) # -C.legsub(c1,c2)
+    array([ 2.,  0., -2.])
+
+    """
+    return pu._sub(c1, c2)
+
+
+def legmulx(c):
+    """Multiply a Legendre series by x.
+
+    Multiply the Legendre series `c` by x, where x is the independent
+    variable.
+
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Legendre series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Array representing the result of the multiplication.
+
+    See Also
+    --------
+    legadd, legmul, legdiv, legpow
+
+    Notes
+    -----
+    The multiplication uses the recursion relationship for Legendre
+    polynomials in the form
+
+    .. math::
+
+      xP_i(x) = ((i + 1)*P_{i + 1}(x) + i*P_{i - 1}(x))/(2i + 1)
+
+    Examples
+    --------
+    >>> from numpy.polynomial import legendre as L
+    >>> L.legmulx([1,2,3])
+    array([ 0.66666667, 2.2, 1.33333333, 1.8]) # may vary
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    # The zero series needs special treatment
+    if len(c) == 1 and c[0] == 0:
+        return c
+
+    prd = np.empty(len(c) + 1, dtype=c.dtype)
+    prd[0] = c[0]*0
+    prd[1] = c[0]
+    for i in range(1, len(c)):
+        j = i + 1
+        k = i - 1
+        s = i + j
+        prd[j] = (c[i]*j)/s
+        prd[k] += (c[i]*i)/s
+    return prd
+
+
+def legmul(c1, c2):
+    """
+    Multiply one Legendre series by another.
+
+    Returns the product of two Legendre series `c1` * `c2`.  The arguments
+    are sequences of coefficients, from lowest order "term" to highest,
+    e.g., [1,2,3] represents the series ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Legendre series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Of Legendre series coefficients representing their product.
+
+    See Also
+    --------
+    legadd, legsub, legmulx, legdiv, legpow
+
+    Notes
+    -----
+    In general, the (polynomial) product of two C-series results in terms
+    that are not in the Legendre polynomial basis set.  Thus, to express
+    the product as a Legendre series, it is necessary to "reproject" the
+    product onto said basis set, which may produce "unintuitive" (but
+    correct) results; see Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial import legendre as L
+    >>> c1 = (1,2,3)
+    >>> c2 = (3,2)
+    >>> L.legmul(c1,c2) # multiplication requires "reprojection"
+    array([  4.33333333,  10.4       ,  11.66666667,   3.6       ]) # may vary
+
+    """
+    # s1, s2 are trimmed copies
+    [c1, c2] = pu.as_series([c1, c2])
+
+    if len(c1) > len(c2):
+        c = c2
+        xs = c1
+    else:
+        c = c1
+        xs = c2
+
+    if len(c) == 1:
+        c0 = c[0]*xs
+        c1 = 0
+    elif len(c) == 2:
+        c0 = c[0]*xs
+        c1 = c[1]*xs
+    else:
+        nd = len(c)
+        c0 = c[-2]*xs
+        c1 = c[-1]*xs
+        for i in range(3, len(c) + 1):
+            tmp = c0
+            nd = nd - 1
+            c0 = legsub(c[-i]*xs, (c1*(nd - 1))/nd)
+            c1 = legadd(tmp, (legmulx(c1)*(2*nd - 1))/nd)
+    return legadd(c0, legmulx(c1))
+
+
+def legdiv(c1, c2):
+    """
+    Divide one Legendre series by another.
+
+    Returns the quotient-with-remainder of two Legendre series
+    `c1` / `c2`.  The arguments are sequences of coefficients from lowest
+    order "term" to highest, e.g., [1,2,3] represents the series
+    ``P_0 + 2*P_1 + 3*P_2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of Legendre series coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    quo, rem : ndarrays
+        Of Legendre series coefficients representing the quotient and
+        remainder.
+
+    See Also
+    --------
+    legadd, legsub, legmulx, legmul, legpow
+
+    Notes
+    -----
+    In general, the (polynomial) division of one Legendre series by another
+    results in quotient and remainder terms that are not in the Legendre
+    polynomial basis set.  Thus, to express these results as a Legendre
+    series, it is necessary to "reproject" the results onto the Legendre
+    basis set, which may produce "unintuitive" (but correct) results; see
+    Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial import legendre as L
+    >>> c1 = (1,2,3)
+    >>> c2 = (3,2,1)
+    >>> L.legdiv(c1,c2) # quotient "intuitive," remainder not
+    (array([3.]), array([-8., -4.]))
+    >>> c2 = (0,1,2,3)
+    >>> L.legdiv(c2,c1) # neither "intuitive"
+    (array([-0.07407407,  1.66666667]), array([-1.03703704, -2.51851852])) # may vary
+
+    """
+    return pu._div(legmul, c1, c2)
+
+
+def legpow(c, pow, maxpower=16):
+    """Raise a Legendre series to a power.
+
+    Returns the Legendre series `c` raised to the power `pow`. The
+    argument `c` is a sequence of coefficients ordered from low to high.
+    i.e., [1,2,3] is the series  ``P_0 + 2*P_1 + 3*P_2.``
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Legendre series coefficients ordered from low to
+        high.
+    pow : integer
+        Power to which the series will be raised
+    maxpower : integer, optional
+        Maximum power allowed. This is mainly to limit growth of the series
+        to unmanageable size. Default is 16
+
+    Returns
+    -------
+    coef : ndarray
+        Legendre series of power.
+
+    See Also
+    --------
+    legadd, legsub, legmulx, legmul, legdiv
+
+    """
+    return pu._pow(legmul, c, pow, maxpower)
+
+
+def legder(c, m=1, scl=1, axis=0):
+    """
+    Differentiate a Legendre series.
+
+    Returns the Legendre series coefficients `c` differentiated `m` times
+    along `axis`.  At each iteration the result is multiplied by `scl` (the
+    scaling factor is for use in a linear change of variable). The argument
+    `c` is an array of coefficients from low to high degree along each
+    axis, e.g., [1,2,3] represents the series ``1*L_0 + 2*L_1 + 3*L_2``
+    while [[1,2],[1,2]] represents ``1*L_0(x)*L_0(y) + 1*L_1(x)*L_0(y) +
+    2*L_0(x)*L_1(y) + 2*L_1(x)*L_1(y)`` if axis=0 is ``x`` and axis=1 is
+    ``y``.
+
+    Parameters
+    ----------
+    c : array_like
+        Array of Legendre series coefficients. If c is multidimensional the
+        different axis correspond to different variables with the degree in
+        each axis given by the corresponding index.
+    m : int, optional
+        Number of derivatives taken, must be non-negative. (Default: 1)
+    scl : scalar, optional
+        Each differentiation is multiplied by `scl`.  The end result is
+        multiplication by ``scl**m``.  This is for use in a linear change of
+        variable. (Default: 1)
+    axis : int, optional
+        Axis over which the derivative is taken. (Default: 0).
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    der : ndarray
+        Legendre series of the derivative.
+
+    See Also
+    --------
+    legint
+
+    Notes
+    -----
+    In general, the result of differentiating a Legendre series does not
+    resemble the same operation on a power series. Thus the result of this
+    function may be "unintuitive," albeit correct; see Examples section
+    below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial import legendre as L
+    >>> c = (1,2,3,4)
+    >>> L.legder(c)
+    array([  6.,   9.,  20.])
+    >>> L.legder(c, 3)
+    array([60.])
+    >>> L.legder(c, scl=-1)
+    array([ -6.,  -9., -20.])
+    >>> L.legder(c, 2,-1)
+    array([  9.,  60.])
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    cnt = pu._deprecate_as_int(m, "the order of derivation")
+    iaxis = pu._deprecate_as_int(axis, "the axis")
+    if cnt < 0:
+        raise ValueError("The order of derivation must be non-negative")
+    iaxis = normalize_axis_index(iaxis, c.ndim)
+
+    if cnt == 0:
+        return c
+
+    c = np.moveaxis(c, iaxis, 0)
+    n = len(c)
+    if cnt >= n:
+        c = c[:1]*0
+    else:
+        for i in range(cnt):
+            n = n - 1
+            c *= scl
+            der = np.empty((n,) + c.shape[1:], dtype=c.dtype)
+            for j in range(n, 2, -1):
+                der[j - 1] = (2*j - 1)*c[j]
+                c[j - 2] += c[j]
+            if n > 1:
+                der[1] = 3*c[2]
+            der[0] = c[1]
+            c = der
+    c = np.moveaxis(c, 0, iaxis)
+    return c
+
+
+def legint(c, m=1, k=[], lbnd=0, scl=1, axis=0):
+    """
+    Integrate a Legendre series.
+
+    Returns the Legendre series coefficients `c` integrated `m` times from
+    `lbnd` along `axis`. At each iteration the resulting series is
+    **multiplied** by `scl` and an integration constant, `k`, is added.
+    The scaling factor is for use in a linear change of variable.  ("Buyer
+    beware": note that, depending on what one is doing, one may want `scl`
+    to be the reciprocal of what one might expect; for more information,
+    see the Notes section below.)  The argument `c` is an array of
+    coefficients from low to high degree along each axis, e.g., [1,2,3]
+    represents the series ``L_0 + 2*L_1 + 3*L_2`` while [[1,2],[1,2]]
+    represents ``1*L_0(x)*L_0(y) + 1*L_1(x)*L_0(y) + 2*L_0(x)*L_1(y) +
+    2*L_1(x)*L_1(y)`` if axis=0 is ``x`` and axis=1 is ``y``.
+
+    Parameters
+    ----------
+    c : array_like
+        Array of Legendre series coefficients. If c is multidimensional the
+        different axis correspond to different variables with the degree in
+        each axis given by the corresponding index.
+    m : int, optional
+        Order of integration, must be positive. (Default: 1)
+    k : {[], list, scalar}, optional
+        Integration constant(s).  The value of the first integral at
+        ``lbnd`` is the first value in the list, the value of the second
+        integral at ``lbnd`` is the second value, etc.  If ``k == []`` (the
+        default), all constants are set to zero.  If ``m == 1``, a single
+        scalar can be given instead of a list.
+    lbnd : scalar, optional
+        The lower bound of the integral. (Default: 0)
+    scl : scalar, optional
+        Following each integration the result is *multiplied* by `scl`
+        before the integration constant is added. (Default: 1)
+    axis : int, optional
+        Axis over which the integral is taken. (Default: 0).
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    S : ndarray
+        Legendre series coefficient array of the integral.
+
+    Raises
+    ------
+    ValueError
+        If ``m < 0``, ``len(k) > m``, ``np.ndim(lbnd) != 0``, or
+        ``np.ndim(scl) != 0``.
+
+    See Also
+    --------
+    legder
+
+    Notes
+    -----
+    Note that the result of each integration is *multiplied* by `scl`.
+    Why is this important to note?  Say one is making a linear change of
+    variable :math:`u = ax + b` in an integral relative to `x`.  Then
+    :math:`dx = du/a`, so one will need to set `scl` equal to
+    :math:`1/a` - perhaps not what one would have first thought.
+
+    Also note that, in general, the result of integrating a C-series needs
+    to be "reprojected" onto the C-series basis set.  Thus, typically,
+    the result of this function is "unintuitive," albeit correct; see
+    Examples section below.
+
+    Examples
+    --------
+    >>> from numpy.polynomial import legendre as L
+    >>> c = (1,2,3)
+    >>> L.legint(c)
+    array([ 0.33333333,  0.4       ,  0.66666667,  0.6       ]) # may vary
+    >>> L.legint(c, 3)
+    array([  1.66666667e-02,  -1.78571429e-02,   4.76190476e-02, # may vary
+             -1.73472348e-18,   1.90476190e-02,   9.52380952e-03])
+    >>> L.legint(c, k=3)
+     array([ 3.33333333,  0.4       ,  0.66666667,  0.6       ]) # may vary
+    >>> L.legint(c, lbnd=-2)
+    array([ 7.33333333,  0.4       ,  0.66666667,  0.6       ]) # may vary
+    >>> L.legint(c, scl=2)
+    array([ 0.66666667,  0.8       ,  1.33333333,  1.2       ]) # may vary
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    if not np.iterable(k):
+        k = [k]
+    cnt = pu._deprecate_as_int(m, "the order of integration")
+    iaxis = pu._deprecate_as_int(axis, "the axis")
+    if cnt < 0:
+        raise ValueError("The order of integration must be non-negative")
+    if len(k) > cnt:
+        raise ValueError("Too many integration constants")
+    if np.ndim(lbnd) != 0:
+        raise ValueError("lbnd must be a scalar.")
+    if np.ndim(scl) != 0:
+        raise ValueError("scl must be a scalar.")
+    iaxis = normalize_axis_index(iaxis, c.ndim)
+
+    if cnt == 0:
+        return c
+
+    c = np.moveaxis(c, iaxis, 0)
+    k = list(k) + [0]*(cnt - len(k))
+    for i in range(cnt):
+        n = len(c)
+        c *= scl
+        if n == 1 and np.all(c[0] == 0):
+            c[0] += k[i]
+        else:
+            tmp = np.empty((n + 1,) + c.shape[1:], dtype=c.dtype)
+            tmp[0] = c[0]*0
+            tmp[1] = c[0]
+            if n > 1:
+                tmp[2] = c[1]/3
+            for j in range(2, n):
+                t = c[j]/(2*j + 1)
+                tmp[j + 1] = t
+                tmp[j - 1] -= t
+            tmp[0] += k[i] - legval(lbnd, tmp)
+            c = tmp
+    c = np.moveaxis(c, 0, iaxis)
+    return c
+
+
+def legval(x, c, tensor=True):
+    """
+    Evaluate a Legendre series at points x.
+
+    If `c` is of length `n + 1`, this function returns the value:
+
+    .. math:: p(x) = c_0 * L_0(x) + c_1 * L_1(x) + ... + c_n * L_n(x)
+
+    The parameter `x` is converted to an array only if it is a tuple or a
+    list, otherwise it is treated as a scalar. In either case, either `x`
+    or its elements must support multiplication and addition both with
+    themselves and with the elements of `c`.
+
+    If `c` is a 1-D array, then `p(x)` will have the same shape as `x`.  If
+    `c` is multidimensional, then the shape of the result depends on the
+    value of `tensor`. If `tensor` is true the shape will be c.shape[1:] +
+    x.shape. If `tensor` is false the shape will be c.shape[1:]. Note that
+    scalars have shape (,).
+
+    Trailing zeros in the coefficients will be used in the evaluation, so
+    they should be avoided if efficiency is a concern.
+
+    Parameters
+    ----------
+    x : array_like, compatible object
+        If `x` is a list or tuple, it is converted to an ndarray, otherwise
+        it is left unchanged and treated as a scalar. In either case, `x`
+        or its elements must support addition and multiplication with
+        themselves and with the elements of `c`.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree n are contained in c[n]. If `c` is multidimensional the
+        remaining indices enumerate multiple polynomials. In the two
+        dimensional case the coefficients may be thought of as stored in
+        the columns of `c`.
+    tensor : boolean, optional
+        If True, the shape of the coefficient array is extended with ones
+        on the right, one for each dimension of `x`. Scalars have dimension 0
+        for this action. The result is that every column of coefficients in
+        `c` is evaluated for every element of `x`. If False, `x` is broadcast
+        over the columns of `c` for the evaluation.  This keyword is useful
+        when `c` is multidimensional. The default value is True.
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    values : ndarray, algebra_like
+        The shape of the return value is described above.
+
+    See Also
+    --------
+    legval2d, leggrid2d, legval3d, leggrid3d
+
+    Notes
+    -----
+    The evaluation uses Clenshaw recursion, aka synthetic division.
+
+    """
+    c = np.array(c, ndmin=1, copy=False)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        c = c.astype(np.double)
+    if isinstance(x, (tuple, list)):
+        x = np.asarray(x)
+    if isinstance(x, np.ndarray) and tensor:
+        c = c.reshape(c.shape + (1,)*x.ndim)
+
+    if len(c) == 1:
+        c0 = c[0]
+        c1 = 0
+    elif len(c) == 2:
+        c0 = c[0]
+        c1 = c[1]
+    else:
+        nd = len(c)
+        c0 = c[-2]
+        c1 = c[-1]
+        for i in range(3, len(c) + 1):
+            tmp = c0
+            nd = nd - 1
+            c0 = c[-i] - (c1*(nd - 1))/nd
+            c1 = tmp + (c1*x*(2*nd - 1))/nd
+    return c0 + c1*x
+
+
+def legval2d(x, y, c):
+    """
+    Evaluate a 2-D Legendre series at points (x, y).
+
+    This function returns the values:
+
+    .. math:: p(x,y) = \\sum_{i,j} c_{i,j} * L_i(x) * L_j(y)
+
+    The parameters `x` and `y` are converted to arrays only if they are
+    tuples or a lists, otherwise they are treated as a scalars and they
+    must have the same shape after conversion. In either case, either `x`
+    and `y` or their elements must support multiplication and addition both
+    with themselves and with the elements of `c`.
+
+    If `c` is a 1-D array a one is implicitly appended to its shape to make
+    it 2-D. The shape of the result will be c.shape[2:] + x.shape.
+
+    Parameters
+    ----------
+    x, y : array_like, compatible objects
+        The two dimensional series is evaluated at the points `(x, y)`,
+        where `x` and `y` must have the same shape. If `x` or `y` is a list
+        or tuple, it is first converted to an ndarray, otherwise it is left
+        unchanged and if it isn't an ndarray it is treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term
+        of multi-degree i,j is contained in ``c[i,j]``. If `c` has
+        dimension greater than two the remaining indices enumerate multiple
+        sets of coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional Legendre series at points formed
+        from pairs of corresponding values from `x` and `y`.
+
+    See Also
+    --------
+    legval, leggrid2d, legval3d, leggrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._valnd(legval, c, x, y)
+
+
+def leggrid2d(x, y, c):
+    """
+    Evaluate a 2-D Legendre series on the Cartesian product of x and y.
+
+    This function returns the values:
+
+    .. math:: p(a,b) = \\sum_{i,j} c_{i,j} * L_i(a) * L_j(b)
+
+    where the points `(a, b)` consist of all pairs formed by taking
+    `a` from `x` and `b` from `y`. The resulting points form a grid with
+    `x` in the first dimension and `y` in the second.
+
+    The parameters `x` and `y` are converted to arrays only if they are
+    tuples or a lists, otherwise they are treated as a scalars. In either
+    case, either `x` and `y` or their elements must support multiplication
+    and addition both with themselves and with the elements of `c`.
+
+    If `c` has fewer than two dimensions, ones are implicitly appended to
+    its shape to make it 2-D. The shape of the result will be c.shape[2:] +
+    x.shape + y.shape.
+
+    Parameters
+    ----------
+    x, y : array_like, compatible objects
+        The two dimensional series is evaluated at the points in the
+        Cartesian product of `x` and `y`.  If `x` or `y` is a list or
+        tuple, it is first converted to an ndarray, otherwise it is left
+        unchanged and, if it isn't an ndarray, it is treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term of
+        multi-degree i,j is contained in `c[i,j]`. If `c` has dimension
+        greater than two the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional Chebyshev series at points in the
+        Cartesian product of `x` and `y`.
+
+    See Also
+    --------
+    legval, legval2d, legval3d, leggrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._gridnd(legval, c, x, y)
+
+
+def legval3d(x, y, z, c):
+    """
+    Evaluate a 3-D Legendre series at points (x, y, z).
+
+    This function returns the values:
+
+    .. math:: p(x,y,z) = \\sum_{i,j,k} c_{i,j,k} * L_i(x) * L_j(y) * L_k(z)
+
+    The parameters `x`, `y`, and `z` are converted to arrays only if
+    they are tuples or a lists, otherwise they are treated as a scalars and
+    they must have the same shape after conversion. In either case, either
+    `x`, `y`, and `z` or their elements must support multiplication and
+    addition both with themselves and with the elements of `c`.
+
+    If `c` has fewer than 3 dimensions, ones are implicitly appended to its
+    shape to make it 3-D. The shape of the result will be c.shape[3:] +
+    x.shape.
+
+    Parameters
+    ----------
+    x, y, z : array_like, compatible object
+        The three dimensional series is evaluated at the points
+        `(x, y, z)`, where `x`, `y`, and `z` must have the same shape.  If
+        any of `x`, `y`, or `z` is a list or tuple, it is first converted
+        to an ndarray, otherwise it is left unchanged and if it isn't an
+        ndarray it is  treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term of
+        multi-degree i,j,k is contained in ``c[i,j,k]``. If `c` has dimension
+        greater than 3 the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the multidimensional polynomial on points formed with
+        triples of corresponding values from `x`, `y`, and `z`.
+
+    See Also
+    --------
+    legval, legval2d, leggrid2d, leggrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._valnd(legval, c, x, y, z)
+
+
+def leggrid3d(x, y, z, c):
+    """
+    Evaluate a 3-D Legendre series on the Cartesian product of x, y, and z.
+
+    This function returns the values:
+
+    .. math:: p(a,b,c) = \\sum_{i,j,k} c_{i,j,k} * L_i(a) * L_j(b) * L_k(c)
+
+    where the points `(a, b, c)` consist of all triples formed by taking
+    `a` from `x`, `b` from `y`, and `c` from `z`. The resulting points form
+    a grid with `x` in the first dimension, `y` in the second, and `z` in
+    the third.
+
+    The parameters `x`, `y`, and `z` are converted to arrays only if they
+    are tuples or a lists, otherwise they are treated as a scalars. In
+    either case, either `x`, `y`, and `z` or their elements must support
+    multiplication and addition both with themselves and with the elements
+    of `c`.
+
+    If `c` has fewer than three dimensions, ones are implicitly appended to
+    its shape to make it 3-D. The shape of the result will be c.shape[3:] +
+    x.shape + y.shape + z.shape.
+
+    Parameters
+    ----------
+    x, y, z : array_like, compatible objects
+        The three dimensional series is evaluated at the points in the
+        Cartesian product of `x`, `y`, and `z`.  If `x`,`y`, or `z` is a
+        list or tuple, it is first converted to an ndarray, otherwise it is
+        left unchanged and, if it isn't an ndarray, it is treated as a
+        scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree i,j are contained in ``c[i,j]``. If `c` has dimension
+        greater than two the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points in the Cartesian
+        product of `x` and `y`.
+
+    See Also
+    --------
+    legval, legval2d, leggrid2d, legval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._gridnd(legval, c, x, y, z)
+
+
+def legvander(x, deg):
+    """Pseudo-Vandermonde matrix of given degree.
+
+    Returns the pseudo-Vandermonde matrix of degree `deg` and sample points
+    `x`. The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., i] = L_i(x)
+
+    where `0 <= i <= deg`. The leading indices of `V` index the elements of
+    `x` and the last index is the degree of the Legendre polynomial.
+
+    If `c` is a 1-D array of coefficients of length `n + 1` and `V` is the
+    array ``V = legvander(x, n)``, then ``np.dot(V, c)`` and
+    ``legval(x, c)`` are the same up to roundoff. This equivalence is
+    useful both for least squares fitting and for the evaluation of a large
+    number of Legendre series of the same degree and sample points.
+
+    Parameters
+    ----------
+    x : array_like
+        Array of points. The dtype is converted to float64 or complex128
+        depending on whether any of the elements are complex. If `x` is
+        scalar it is converted to a 1-D array.
+    deg : int
+        Degree of the resulting matrix.
+
+    Returns
+    -------
+    vander : ndarray
+        The pseudo-Vandermonde matrix. The shape of the returned matrix is
+        ``x.shape + (deg + 1,)``, where The last index is the degree of the
+        corresponding Legendre polynomial.  The dtype will be the same as
+        the converted `x`.
+
+    """
+    ideg = pu._deprecate_as_int(deg, "deg")
+    if ideg < 0:
+        raise ValueError("deg must be non-negative")
+
+    x = np.array(x, copy=False, ndmin=1) + 0.0
+    dims = (ideg + 1,) + x.shape
+    dtyp = x.dtype
+    v = np.empty(dims, dtype=dtyp)
+    # Use forward recursion to generate the entries. This is not as accurate
+    # as reverse recursion in this application but it is more efficient.
+    v[0] = x*0 + 1
+    if ideg > 0:
+        v[1] = x
+        for i in range(2, ideg + 1):
+            v[i] = (v[i-1]*x*(2*i - 1) - v[i-2]*(i - 1))/i
+    return np.moveaxis(v, 0, -1)
+
+
+def legvander2d(x, y, deg):
+    """Pseudo-Vandermonde matrix of given degrees.
+
+    Returns the pseudo-Vandermonde matrix of degrees `deg` and sample
+    points `(x, y)`. The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., (deg[1] + 1)*i + j] = L_i(x) * L_j(y),
+
+    where `0 <= i <= deg[0]` and `0 <= j <= deg[1]`. The leading indices of
+    `V` index the points `(x, y)` and the last index encodes the degrees of
+    the Legendre polynomials.
+
+    If ``V = legvander2d(x, y, [xdeg, ydeg])``, then the columns of `V`
+    correspond to the elements of a 2-D coefficient array `c` of shape
+    (xdeg + 1, ydeg + 1) in the order
+
+    .. math:: c_{00}, c_{01}, c_{02} ... , c_{10}, c_{11}, c_{12} ...
+
+    and ``np.dot(V, c.flat)`` and ``legval2d(x, y, c)`` will be the same
+    up to roundoff. This equivalence is useful both for least squares
+    fitting and for the evaluation of a large number of 2-D Legendre
+    series of the same degrees and sample points.
+
+    Parameters
+    ----------
+    x, y : array_like
+        Arrays of point coordinates, all of the same shape. The dtypes
+        will be converted to either float64 or complex128 depending on
+        whether any of the elements are complex. Scalars are converted to
+        1-D arrays.
+    deg : list of ints
+        List of maximum degrees of the form [x_deg, y_deg].
+
+    Returns
+    -------
+    vander2d : ndarray
+        The shape of the returned matrix is ``x.shape + (order,)``, where
+        :math:`order = (deg[0]+1)*(deg[1]+1)`.  The dtype will be the same
+        as the converted `x` and `y`.
+
+    See Also
+    --------
+    legvander, legvander3d, legval2d, legval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._vander_nd_flat((legvander, legvander), (x, y), deg)
+
+
+def legvander3d(x, y, z, deg):
+    """Pseudo-Vandermonde matrix of given degrees.
+
+    Returns the pseudo-Vandermonde matrix of degrees `deg` and sample
+    points `(x, y, z)`. If `l, m, n` are the given degrees in `x, y, z`,
+    then The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., (m+1)(n+1)i + (n+1)j + k] = L_i(x)*L_j(y)*L_k(z),
+
+    where `0 <= i <= l`, `0 <= j <= m`, and `0 <= j <= n`.  The leading
+    indices of `V` index the points `(x, y, z)` and the last index encodes
+    the degrees of the Legendre polynomials.
+
+    If ``V = legvander3d(x, y, z, [xdeg, ydeg, zdeg])``, then the columns
+    of `V` correspond to the elements of a 3-D coefficient array `c` of
+    shape (xdeg + 1, ydeg + 1, zdeg + 1) in the order
+
+    .. math:: c_{000}, c_{001}, c_{002},... , c_{010}, c_{011}, c_{012},...
+
+    and ``np.dot(V, c.flat)`` and ``legval3d(x, y, z, c)`` will be the
+    same up to roundoff. This equivalence is useful both for least squares
+    fitting and for the evaluation of a large number of 3-D Legendre
+    series of the same degrees and sample points.
+
+    Parameters
+    ----------
+    x, y, z : array_like
+        Arrays of point coordinates, all of the same shape. The dtypes will
+        be converted to either float64 or complex128 depending on whether
+        any of the elements are complex. Scalars are converted to 1-D
+        arrays.
+    deg : list of ints
+        List of maximum degrees of the form [x_deg, y_deg, z_deg].
+
+    Returns
+    -------
+    vander3d : ndarray
+        The shape of the returned matrix is ``x.shape + (order,)``, where
+        :math:`order = (deg[0]+1)*(deg[1]+1)*(deg[2]+1)`.  The dtype will
+        be the same as the converted `x`, `y`, and `z`.
+
+    See Also
+    --------
+    legvander, legvander3d, legval2d, legval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._vander_nd_flat((legvander, legvander, legvander), (x, y, z), deg)
+
+
+def legfit(x, y, deg, rcond=None, full=False, w=None):
+    """
+    Least squares fit of Legendre series to data.
+
+    Return the coefficients of a Legendre series of degree `deg` that is the
+    least squares fit to the data values `y` given at points `x`. If `y` is
+    1-D the returned coefficients will also be 1-D. If `y` is 2-D multiple
+    fits are done, one for each column of `y`, and the resulting
+    coefficients are stored in the corresponding columns of a 2-D return.
+    The fitted polynomial(s) are in the form
+
+    .. math::  p(x) = c_0 + c_1 * L_1(x) + ... + c_n * L_n(x),
+
+    where `n` is `deg`.
+
+    Parameters
+    ----------
+    x : array_like, shape (M,)
+        x-coordinates of the M sample points ``(x[i], y[i])``.
+    y : array_like, shape (M,) or (M, K)
+        y-coordinates of the sample points. Several data sets of sample
+        points sharing the same x-coordinates can be fitted at once by
+        passing in a 2D-array that contains one dataset per column.
+    deg : int or 1-D array_like
+        Degree(s) of the fitting polynomials. If `deg` is a single integer
+        all terms up to and including the `deg`'th term are included in the
+        fit. For NumPy versions >= 1.11.0 a list of integers specifying the
+        degrees of the terms to include may be used instead.
+    rcond : float, optional
+        Relative condition number of the fit. Singular values smaller than
+        this relative to the largest singular value will be ignored. The
+        default value is len(x)*eps, where eps is the relative precision of
+        the float type, about 2e-16 in most cases.
+    full : bool, optional
+        Switch determining nature of return value. When it is False (the
+        default) just the coefficients are returned, when True diagnostic
+        information from the singular value decomposition is also returned.
+    w : array_like, shape (`M`,), optional
+        Weights. If not None, the weight ``w[i]`` applies to the unsquared
+        residual ``y[i] - y_hat[i]`` at ``x[i]``. Ideally the weights are
+        chosen so that the errors of the products ``w[i]*y[i]`` all have the
+        same variance.  When using inverse-variance weighting, use
+        ``w[i] = 1/sigma(y[i])``.  The default value is None.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    coef : ndarray, shape (M,) or (M, K)
+        Legendre coefficients ordered from low to high. If `y` was
+        2-D, the coefficients for the data in column k of `y` are in
+        column `k`. If `deg` is specified as a list, coefficients for
+        terms not included in the fit are set equal to zero in the
+        returned `coef`.
+
+    [residuals, rank, singular_values, rcond] : list
+        These values are only returned if ``full == True``
+
+        - residuals -- sum of squared residuals of the least squares fit
+        - rank -- the numerical rank of the scaled Vandermonde matrix
+        - singular_values -- singular values of the scaled Vandermonde matrix
+        - rcond -- value of `rcond`.
+
+        For more details, see `numpy.linalg.lstsq`.
+
+    Warns
+    -----
+    RankWarning
+        The rank of the coefficient matrix in the least-squares fit is
+        deficient. The warning is only raised if ``full == False``.  The
+        warnings can be turned off by
+
+        >>> import warnings
+        >>> warnings.simplefilter('ignore', np.RankWarning)
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyfit
+    numpy.polynomial.chebyshev.chebfit
+    numpy.polynomial.laguerre.lagfit
+    numpy.polynomial.hermite.hermfit
+    numpy.polynomial.hermite_e.hermefit
+    legval : Evaluates a Legendre series.
+    legvander : Vandermonde matrix of Legendre series.
+    legweight : Legendre weight function (= 1).
+    numpy.linalg.lstsq : Computes a least-squares fit from the matrix.
+    scipy.interpolate.UnivariateSpline : Computes spline fits.
+
+    Notes
+    -----
+    The solution is the coefficients of the Legendre series `p` that
+    minimizes the sum of the weighted squared errors
+
+    .. math:: E = \\sum_j w_j^2 * |y_j - p(x_j)|^2,
+
+    where :math:`w_j` are the weights. This problem is solved by setting up
+    as the (typically) overdetermined matrix equation
+
+    .. math:: V(x) * c = w * y,
+
+    where `V` is the weighted pseudo Vandermonde matrix of `x`, `c` are the
+    coefficients to be solved for, `w` are the weights, and `y` are the
+    observed values.  This equation is then solved using the singular value
+    decomposition of `V`.
+
+    If some of the singular values of `V` are so small that they are
+    neglected, then a `RankWarning` will be issued. This means that the
+    coefficient values may be poorly determined. Using a lower order fit
+    will usually get rid of the warning.  The `rcond` parameter can also be
+    set to a value smaller than its default, but the resulting fit may be
+    spurious and have large contributions from roundoff error.
+
+    Fits using Legendre series are usually better conditioned than fits
+    using power series, but much can depend on the distribution of the
+    sample points and the smoothness of the data. If the quality of the fit
+    is inadequate splines may be a good alternative.
+
+    References
+    ----------
+    .. [1] Wikipedia, "Curve fitting",
+           https://en.wikipedia.org/wiki/Curve_fitting
+
+    Examples
+    --------
+
+    """
+    return pu._fit(legvander, x, y, deg, rcond, full, w)
+
+
+def legcompanion(c):
+    """Return the scaled companion matrix of c.
+
+    The basis polynomials are scaled so that the companion matrix is
+    symmetric when `c` is an Legendre basis polynomial. This provides
+    better eigenvalue estimates than the unscaled case and for basis
+    polynomials the eigenvalues are guaranteed to be real if
+    `numpy.linalg.eigvalsh` is used to obtain them.
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of Legendre series coefficients ordered from low to high
+        degree.
+
+    Returns
+    -------
+    mat : ndarray
+        Scaled companion matrix of dimensions (deg, deg).
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    if len(c) < 2:
+        raise ValueError('Series must have maximum degree of at least 1.')
+    if len(c) == 2:
+        return np.array([[-c[0]/c[1]]])
+
+    n = len(c) - 1
+    mat = np.zeros((n, n), dtype=c.dtype)
+    scl = 1./np.sqrt(2*np.arange(n) + 1)
+    top = mat.reshape(-1)[1::n+1]
+    bot = mat.reshape(-1)[n::n+1]
+    top[...] = np.arange(1, n)*scl[:n-1]*scl[1:n]
+    bot[...] = top
+    mat[:, -1] -= (c[:-1]/c[-1])*(scl/scl[-1])*(n/(2*n - 1))
+    return mat
+
+
+def legroots(c):
+    """
+    Compute the roots of a Legendre series.
+
+    Return the roots (a.k.a. "zeros") of the polynomial
+
+    .. math:: p(x) = \\sum_i c[i] * L_i(x).
+
+    Parameters
+    ----------
+    c : 1-D array_like
+        1-D array of coefficients.
+
+    Returns
+    -------
+    out : ndarray
+        Array of the roots of the series. If all the roots are real,
+        then `out` is also real, otherwise it is complex.
+
+    See Also
+    --------
+    numpy.polynomial.polynomial.polyroots
+    numpy.polynomial.chebyshev.chebroots
+    numpy.polynomial.laguerre.lagroots
+    numpy.polynomial.hermite.hermroots
+    numpy.polynomial.hermite_e.hermeroots
+
+    Notes
+    -----
+    The root estimates are obtained as the eigenvalues of the companion
+    matrix, Roots far from the origin of the complex plane may have large
+    errors due to the numerical instability of the series for such values.
+    Roots with multiplicity greater than 1 will also show larger errors as
+    the value of the series near such points is relatively insensitive to
+    errors in the roots. Isolated roots near the origin can be improved by
+    a few iterations of Newton's method.
+
+    The Legendre series basis polynomials aren't powers of ``x`` so the
+    results of this function may seem unintuitive.
+
+    Examples
+    --------
+    >>> import numpy.polynomial.legendre as leg
+    >>> leg.legroots((1, 2, 3, 4)) # 4L_3 + 3L_2 + 2L_1 + 1L_0, all real roots
+    array([-0.85099543, -0.11407192,  0.51506735]) # may vary
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    if len(c) < 2:
+        return np.array([], dtype=c.dtype)
+    if len(c) == 2:
+        return np.array([-c[0]/c[1]])
+
+    # rotated companion matrix reduces error
+    m = legcompanion(c)[::-1,::-1]
+    r = la.eigvals(m)
+    r.sort()
+    return r
+
+
+def leggauss(deg):
+    """
+    Gauss-Legendre quadrature.
+
+    Computes the sample points and weights for Gauss-Legendre quadrature.
+    These sample points and weights will correctly integrate polynomials of
+    degree :math:`2*deg - 1` or less over the interval :math:`[-1, 1]` with
+    the weight function :math:`f(x) = 1`.
+
+    Parameters
+    ----------
+    deg : int
+        Number of sample points and weights. It must be >= 1.
+
+    Returns
+    -------
+    x : ndarray
+        1-D ndarray containing the sample points.
+    y : ndarray
+        1-D ndarray containing the weights.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    The results have only been tested up to degree 100, higher degrees may
+    be problematic. The weights are determined by using the fact that
+
+    .. math:: w_k = c / (L'_n(x_k) * L_{n-1}(x_k))
+
+    where :math:`c` is a constant independent of :math:`k` and :math:`x_k`
+    is the k'th root of :math:`L_n`, and then scaling the results to get
+    the right value when integrating 1.
+
+    """
+    ideg = pu._deprecate_as_int(deg, "deg")
+    if ideg <= 0:
+        raise ValueError("deg must be a positive integer")
+
+    # first approximation of roots. We use the fact that the companion
+    # matrix is symmetric in this case in order to obtain better zeros.
+    c = np.array([0]*deg + [1])
+    m = legcompanion(c)
+    x = la.eigvalsh(m)
+
+    # improve roots by one application of Newton
+    dy = legval(x, c)
+    df = legval(x, legder(c))
+    x -= dy/df
+
+    # compute the weights. We scale the factor to avoid possible numerical
+    # overflow.
+    fm = legval(x, c[1:])
+    fm /= np.abs(fm).max()
+    df /= np.abs(df).max()
+    w = 1/(fm * df)
+
+    # for Legendre we can also symmetrize
+    w = (w + w[::-1])/2
+    x = (x - x[::-1])/2
+
+    # scale w to get the right value
+    w *= 2. / w.sum()
+
+    return x, w
+
+
+def legweight(x):
+    """
+    Weight function of the Legendre polynomials.
+
+    The weight function is :math:`1` and the interval of integration is
+    :math:`[-1, 1]`. The Legendre polynomials are orthogonal, but not
+    normalized, with respect to this weight function.
+
+    Parameters
+    ----------
+    x : array_like
+       Values at which the weight function will be computed.
+
+    Returns
+    -------
+    w : ndarray
+       The weight function at `x`.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    w = x*0.0 + 1.0
+    return w
+
+#
+# Legendre series class
+#
+
+class Legendre(ABCPolyBase):
+    """A Legendre series class.
+
+    The Legendre class provides the standard Python numerical methods
+    '+', '-', '*', '//', '%', 'divmod', '**', and '()' as well as the
+    attributes and methods listed in the `ABCPolyBase` documentation.
+
+    Parameters
+    ----------
+    coef : array_like
+        Legendre coefficients in order of increasing degree, i.e.,
+        ``(1, 2, 3)`` gives ``1*P_0(x) + 2*P_1(x) + 3*P_2(x)``.
+    domain : (2,) array_like, optional
+        Domain to use. The interval ``[domain[0], domain[1]]`` is mapped
+        to the interval ``[window[0], window[1]]`` by shifting and scaling.
+        The default value is [-1, 1].
+    window : (2,) array_like, optional
+        Window, see `domain` for its use. The default value is [-1, 1].
+
+        .. versionadded:: 1.6.0
+    symbol : str, optional
+        Symbol used to represent the independent variable in string
+        representations of the polynomial expression, e.g. for printing.
+        The symbol must be a valid Python identifier. Default value is 'x'.
+
+        .. versionadded:: 1.24
+
+    """
+    # Virtual Functions
+    _add = staticmethod(legadd)
+    _sub = staticmethod(legsub)
+    _mul = staticmethod(legmul)
+    _div = staticmethod(legdiv)
+    _pow = staticmethod(legpow)
+    _val = staticmethod(legval)
+    _int = staticmethod(legint)
+    _der = staticmethod(legder)
+    _fit = staticmethod(legfit)
+    _line = staticmethod(legline)
+    _roots = staticmethod(legroots)
+    _fromroots = staticmethod(legfromroots)
+
+    # Virtual properties
+    domain = np.array(legdomain)
+    window = np.array(legdomain)
+    basis_name = 'P'
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/legendre.pyi b/.venv/lib/python3.12/site-packages/numpy/polynomial/legendre.pyi
new file mode 100644
index 00000000..63a1c3f3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/legendre.pyi
@@ -0,0 +1,46 @@
+from typing import Any
+
+from numpy import ndarray, dtype, int_
+from numpy.polynomial._polybase import ABCPolyBase
+from numpy.polynomial.polyutils import trimcoef
+
+__all__: list[str]
+
+legtrim = trimcoef
+
+def poly2leg(pol): ...
+def leg2poly(c): ...
+
+legdomain: ndarray[Any, dtype[int_]]
+legzero: ndarray[Any, dtype[int_]]
+legone: ndarray[Any, dtype[int_]]
+legx: ndarray[Any, dtype[int_]]
+
+def legline(off, scl): ...
+def legfromroots(roots): ...
+def legadd(c1, c2): ...
+def legsub(c1, c2): ...
+def legmulx(c): ...
+def legmul(c1, c2): ...
+def legdiv(c1, c2): ...
+def legpow(c, pow, maxpower=...): ...
+def legder(c, m=..., scl=..., axis=...): ...
+def legint(c, m=..., k = ..., lbnd=..., scl=..., axis=...): ...
+def legval(x, c, tensor=...): ...
+def legval2d(x, y, c): ...
+def leggrid2d(x, y, c): ...
+def legval3d(x, y, z, c): ...
+def leggrid3d(x, y, z, c): ...
+def legvander(x, deg): ...
+def legvander2d(x, y, deg): ...
+def legvander3d(x, y, z, deg): ...
+def legfit(x, y, deg, rcond=..., full=..., w=...): ...
+def legcompanion(c): ...
+def legroots(c): ...
+def leggauss(deg): ...
+def legweight(x): ...
+
+class Legendre(ABCPolyBase):
+    domain: Any
+    window: Any
+    basis_name: Any
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/polynomial.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/polynomial.py
new file mode 100644
index 00000000..ceadff0b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/polynomial.py
@@ -0,0 +1,1542 @@
+"""
+=================================================
+Power Series (:mod:`numpy.polynomial.polynomial`)
+=================================================
+
+This module provides a number of objects (mostly functions) useful for
+dealing with polynomials, including a `Polynomial` class that
+encapsulates the usual arithmetic operations.  (General information
+on how this module represents and works with polynomial objects is in
+the docstring for its "parent" sub-package, `numpy.polynomial`).
+
+Classes
+-------
+.. autosummary::
+   :toctree: generated/
+
+   Polynomial
+
+Constants
+---------
+.. autosummary::
+   :toctree: generated/
+
+   polydomain
+   polyzero
+   polyone
+   polyx
+
+Arithmetic
+----------
+.. autosummary::
+   :toctree: generated/
+
+   polyadd
+   polysub
+   polymulx
+   polymul
+   polydiv
+   polypow
+   polyval
+   polyval2d
+   polyval3d
+   polygrid2d
+   polygrid3d
+
+Calculus
+--------
+.. autosummary::
+   :toctree: generated/
+
+   polyder
+   polyint
+
+Misc Functions
+--------------
+.. autosummary::
+   :toctree: generated/
+
+   polyfromroots
+   polyroots
+   polyvalfromroots
+   polyvander
+   polyvander2d
+   polyvander3d
+   polycompanion
+   polyfit
+   polytrim
+   polyline
+
+See Also
+--------
+`numpy.polynomial`
+
+"""
+__all__ = [
+    'polyzero', 'polyone', 'polyx', 'polydomain', 'polyline', 'polyadd',
+    'polysub', 'polymulx', 'polymul', 'polydiv', 'polypow', 'polyval',
+    'polyvalfromroots', 'polyder', 'polyint', 'polyfromroots', 'polyvander',
+    'polyfit', 'polytrim', 'polyroots', 'Polynomial', 'polyval2d', 'polyval3d',
+    'polygrid2d', 'polygrid3d', 'polyvander2d', 'polyvander3d']
+
+import numpy as np
+import numpy.linalg as la
+from numpy.core.multiarray import normalize_axis_index
+
+from . import polyutils as pu
+from ._polybase import ABCPolyBase
+
+polytrim = pu.trimcoef
+
+#
+# These are constant arrays are of integer type so as to be compatible
+# with the widest range of other types, such as Decimal.
+#
+
+# Polynomial default domain.
+polydomain = np.array([-1, 1])
+
+# Polynomial coefficients representing zero.
+polyzero = np.array([0])
+
+# Polynomial coefficients representing one.
+polyone = np.array([1])
+
+# Polynomial coefficients representing the identity x.
+polyx = np.array([0, 1])
+
+#
+# Polynomial series functions
+#
+
+
+def polyline(off, scl):
+    """
+    Returns an array representing a linear polynomial.
+
+    Parameters
+    ----------
+    off, scl : scalars
+        The "y-intercept" and "slope" of the line, respectively.
+
+    Returns
+    -------
+    y : ndarray
+        This module's representation of the linear polynomial ``off +
+        scl*x``.
+
+    See Also
+    --------
+    numpy.polynomial.chebyshev.chebline
+    numpy.polynomial.legendre.legline
+    numpy.polynomial.laguerre.lagline
+    numpy.polynomial.hermite.hermline
+    numpy.polynomial.hermite_e.hermeline
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polynomial as P
+    >>> P.polyline(1,-1)
+    array([ 1, -1])
+    >>> P.polyval(1, P.polyline(1,-1)) # should be 0
+    0.0
+
+    """
+    if scl != 0:
+        return np.array([off, scl])
+    else:
+        return np.array([off])
+
+
+def polyfromroots(roots):
+    """
+    Generate a monic polynomial with given roots.
+
+    Return the coefficients of the polynomial
+
+    .. math:: p(x) = (x - r_0) * (x - r_1) * ... * (x - r_n),
+
+    where the ``r_n`` are the roots specified in `roots`.  If a zero has
+    multiplicity n, then it must appear in `roots` n times. For instance,
+    if 2 is a root of multiplicity three and 3 is a root of multiplicity 2,
+    then `roots` looks something like [2, 2, 2, 3, 3]. The roots can appear
+    in any order.
+
+    If the returned coefficients are `c`, then
+
+    .. math:: p(x) = c_0 + c_1 * x + ... +  x^n
+
+    The coefficient of the last term is 1 for monic polynomials in this
+    form.
+
+    Parameters
+    ----------
+    roots : array_like
+        Sequence containing the roots.
+
+    Returns
+    -------
+    out : ndarray
+        1-D array of the polynomial's coefficients If all the roots are
+        real, then `out` is also real, otherwise it is complex.  (see
+        Examples below).
+
+    See Also
+    --------
+    numpy.polynomial.chebyshev.chebfromroots
+    numpy.polynomial.legendre.legfromroots
+    numpy.polynomial.laguerre.lagfromroots
+    numpy.polynomial.hermite.hermfromroots
+    numpy.polynomial.hermite_e.hermefromroots
+
+    Notes
+    -----
+    The coefficients are determined by multiplying together linear factors
+    of the form ``(x - r_i)``, i.e.
+
+    .. math:: p(x) = (x - r_0) (x - r_1) ... (x - r_n)
+
+    where ``n == len(roots) - 1``; note that this implies that ``1`` is always
+    returned for :math:`a_n`.
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polynomial as P
+    >>> P.polyfromroots((-1,0,1)) # x(x - 1)(x + 1) = x^3 - x
+    array([ 0., -1.,  0.,  1.])
+    >>> j = complex(0,1)
+    >>> P.polyfromroots((-j,j)) # complex returned, though values are real
+    array([1.+0.j,  0.+0.j,  1.+0.j])
+
+    """
+    return pu._fromroots(polyline, polymul, roots)
+
+
+def polyadd(c1, c2):
+    """
+    Add one polynomial to another.
+
+    Returns the sum of two polynomials `c1` + `c2`.  The arguments are
+    sequences of coefficients from lowest order term to highest, i.e.,
+    [1,2,3] represents the polynomial ``1 + 2*x + 3*x**2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of polynomial coefficients ordered from low to high.
+
+    Returns
+    -------
+    out : ndarray
+        The coefficient array representing their sum.
+
+    See Also
+    --------
+    polysub, polymulx, polymul, polydiv, polypow
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polynomial as P
+    >>> c1 = (1,2,3)
+    >>> c2 = (3,2,1)
+    >>> sum = P.polyadd(c1,c2); sum
+    array([4.,  4.,  4.])
+    >>> P.polyval(2, sum) # 4 + 4(2) + 4(2**2)
+    28.0
+
+    """
+    return pu._add(c1, c2)
+
+
+def polysub(c1, c2):
+    """
+    Subtract one polynomial from another.
+
+    Returns the difference of two polynomials `c1` - `c2`.  The arguments
+    are sequences of coefficients from lowest order term to highest, i.e.,
+    [1,2,3] represents the polynomial ``1 + 2*x + 3*x**2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of polynomial coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Of coefficients representing their difference.
+
+    See Also
+    --------
+    polyadd, polymulx, polymul, polydiv, polypow
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polynomial as P
+    >>> c1 = (1,2,3)
+    >>> c2 = (3,2,1)
+    >>> P.polysub(c1,c2)
+    array([-2.,  0.,  2.])
+    >>> P.polysub(c2,c1) # -P.polysub(c1,c2)
+    array([ 2.,  0., -2.])
+
+    """
+    return pu._sub(c1, c2)
+
+
+def polymulx(c):
+    """Multiply a polynomial by x.
+
+    Multiply the polynomial `c` by x, where x is the independent
+    variable.
+
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of polynomial coefficients ordered from low to
+        high.
+
+    Returns
+    -------
+    out : ndarray
+        Array representing the result of the multiplication.
+
+    See Also
+    --------
+    polyadd, polysub, polymul, polydiv, polypow
+
+    Notes
+    -----
+
+    .. versionadded:: 1.5.0
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    # The zero series needs special treatment
+    if len(c) == 1 and c[0] == 0:
+        return c
+
+    prd = np.empty(len(c) + 1, dtype=c.dtype)
+    prd[0] = c[0]*0
+    prd[1:] = c
+    return prd
+
+
+def polymul(c1, c2):
+    """
+    Multiply one polynomial by another.
+
+    Returns the product of two polynomials `c1` * `c2`.  The arguments are
+    sequences of coefficients, from lowest order term to highest, e.g.,
+    [1,2,3] represents the polynomial ``1 + 2*x + 3*x**2.``
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of coefficients representing a polynomial, relative to the
+        "standard" basis, and ordered from lowest order term to highest.
+
+    Returns
+    -------
+    out : ndarray
+        Of the coefficients of their product.
+
+    See Also
+    --------
+    polyadd, polysub, polymulx, polydiv, polypow
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polynomial as P
+    >>> c1 = (1,2,3)
+    >>> c2 = (3,2,1)
+    >>> P.polymul(c1,c2)
+    array([  3.,   8.,  14.,   8.,   3.])
+
+    """
+    # c1, c2 are trimmed copies
+    [c1, c2] = pu.as_series([c1, c2])
+    ret = np.convolve(c1, c2)
+    return pu.trimseq(ret)
+
+
+def polydiv(c1, c2):
+    """
+    Divide one polynomial by another.
+
+    Returns the quotient-with-remainder of two polynomials `c1` / `c2`.
+    The arguments are sequences of coefficients, from lowest order term
+    to highest, e.g., [1,2,3] represents ``1 + 2*x + 3*x**2``.
+
+    Parameters
+    ----------
+    c1, c2 : array_like
+        1-D arrays of polynomial coefficients ordered from low to high.
+
+    Returns
+    -------
+    [quo, rem] : ndarrays
+        Of coefficient series representing the quotient and remainder.
+
+    See Also
+    --------
+    polyadd, polysub, polymulx, polymul, polypow
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polynomial as P
+    >>> c1 = (1,2,3)
+    >>> c2 = (3,2,1)
+    >>> P.polydiv(c1,c2)
+    (array([3.]), array([-8., -4.]))
+    >>> P.polydiv(c2,c1)
+    (array([ 0.33333333]), array([ 2.66666667,  1.33333333])) # may vary
+
+    """
+    # c1, c2 are trimmed copies
+    [c1, c2] = pu.as_series([c1, c2])
+    if c2[-1] == 0:
+        raise ZeroDivisionError()
+
+    # note: this is more efficient than `pu._div(polymul, c1, c2)`
+    lc1 = len(c1)
+    lc2 = len(c2)
+    if lc1 < lc2:
+        return c1[:1]*0, c1
+    elif lc2 == 1:
+        return c1/c2[-1], c1[:1]*0
+    else:
+        dlen = lc1 - lc2
+        scl = c2[-1]
+        c2 = c2[:-1]/scl
+        i = dlen
+        j = lc1 - 1
+        while i >= 0:
+            c1[i:j] -= c2*c1[j]
+            i -= 1
+            j -= 1
+        return c1[j+1:]/scl, pu.trimseq(c1[:j+1])
+
+
+def polypow(c, pow, maxpower=None):
+    """Raise a polynomial to a power.
+
+    Returns the polynomial `c` raised to the power `pow`. The argument
+    `c` is a sequence of coefficients ordered from low to high. i.e.,
+    [1,2,3] is the series  ``1 + 2*x + 3*x**2.``
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of array of series coefficients ordered from low to
+        high degree.
+    pow : integer
+        Power to which the series will be raised
+    maxpower : integer, optional
+        Maximum power allowed. This is mainly to limit growth of the series
+        to unmanageable size. Default is 16
+
+    Returns
+    -------
+    coef : ndarray
+        Power series of power.
+
+    See Also
+    --------
+    polyadd, polysub, polymulx, polymul, polydiv
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polynomial as P
+    >>> P.polypow([1,2,3], 2)
+    array([ 1., 4., 10., 12., 9.])
+
+    """
+    # note: this is more efficient than `pu._pow(polymul, c1, c2)`, as it
+    # avoids calling `as_series` repeatedly
+    return pu._pow(np.convolve, c, pow, maxpower)
+
+
+def polyder(c, m=1, scl=1, axis=0):
+    """
+    Differentiate a polynomial.
+
+    Returns the polynomial coefficients `c` differentiated `m` times along
+    `axis`.  At each iteration the result is multiplied by `scl` (the
+    scaling factor is for use in a linear change of variable).  The
+    argument `c` is an array of coefficients from low to high degree along
+    each axis, e.g., [1,2,3] represents the polynomial ``1 + 2*x + 3*x**2``
+    while [[1,2],[1,2]] represents ``1 + 1*x + 2*y + 2*x*y`` if axis=0 is
+    ``x`` and axis=1 is ``y``.
+
+    Parameters
+    ----------
+    c : array_like
+        Array of polynomial coefficients. If c is multidimensional the
+        different axis correspond to different variables with the degree
+        in each axis given by the corresponding index.
+    m : int, optional
+        Number of derivatives taken, must be non-negative. (Default: 1)
+    scl : scalar, optional
+        Each differentiation is multiplied by `scl`.  The end result is
+        multiplication by ``scl**m``.  This is for use in a linear change
+        of variable. (Default: 1)
+    axis : int, optional
+        Axis over which the derivative is taken. (Default: 0).
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    der : ndarray
+        Polynomial coefficients of the derivative.
+
+    See Also
+    --------
+    polyint
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polynomial as P
+    >>> c = (1,2,3,4) # 1 + 2x + 3x**2 + 4x**3
+    >>> P.polyder(c) # (d/dx)(c) = 2 + 6x + 12x**2
+    array([  2.,   6.,  12.])
+    >>> P.polyder(c,3) # (d**3/dx**3)(c) = 24
+    array([24.])
+    >>> P.polyder(c,scl=-1) # (d/d(-x))(c) = -2 - 6x - 12x**2
+    array([ -2.,  -6., -12.])
+    >>> P.polyder(c,2,-1) # (d**2/d(-x)**2)(c) = 6 + 24x
+    array([  6.,  24.])
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        # astype fails with NA
+        c = c + 0.0
+    cdt = c.dtype
+    cnt = pu._deprecate_as_int(m, "the order of derivation")
+    iaxis = pu._deprecate_as_int(axis, "the axis")
+    if cnt < 0:
+        raise ValueError("The order of derivation must be non-negative")
+    iaxis = normalize_axis_index(iaxis, c.ndim)
+
+    if cnt == 0:
+        return c
+
+    c = np.moveaxis(c, iaxis, 0)
+    n = len(c)
+    if cnt >= n:
+        c = c[:1]*0
+    else:
+        for i in range(cnt):
+            n = n - 1
+            c *= scl
+            der = np.empty((n,) + c.shape[1:], dtype=cdt)
+            for j in range(n, 0, -1):
+                der[j - 1] = j*c[j]
+            c = der
+    c = np.moveaxis(c, 0, iaxis)
+    return c
+
+
+def polyint(c, m=1, k=[], lbnd=0, scl=1, axis=0):
+    """
+    Integrate a polynomial.
+
+    Returns the polynomial coefficients `c` integrated `m` times from
+    `lbnd` along `axis`.  At each iteration the resulting series is
+    **multiplied** by `scl` and an integration constant, `k`, is added.
+    The scaling factor is for use in a linear change of variable.  ("Buyer
+    beware": note that, depending on what one is doing, one may want `scl`
+    to be the reciprocal of what one might expect; for more information,
+    see the Notes section below.) The argument `c` is an array of
+    coefficients, from low to high degree along each axis, e.g., [1,2,3]
+    represents the polynomial ``1 + 2*x + 3*x**2`` while [[1,2],[1,2]]
+    represents ``1 + 1*x + 2*y + 2*x*y`` if axis=0 is ``x`` and axis=1 is
+    ``y``.
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of polynomial coefficients, ordered from low to high.
+    m : int, optional
+        Order of integration, must be positive. (Default: 1)
+    k : {[], list, scalar}, optional
+        Integration constant(s).  The value of the first integral at zero
+        is the first value in the list, the value of the second integral
+        at zero is the second value, etc.  If ``k == []`` (the default),
+        all constants are set to zero.  If ``m == 1``, a single scalar can
+        be given instead of a list.
+    lbnd : scalar, optional
+        The lower bound of the integral. (Default: 0)
+    scl : scalar, optional
+        Following each integration the result is *multiplied* by `scl`
+        before the integration constant is added. (Default: 1)
+    axis : int, optional
+        Axis over which the integral is taken. (Default: 0).
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    S : ndarray
+        Coefficient array of the integral.
+
+    Raises
+    ------
+    ValueError
+        If ``m < 1``, ``len(k) > m``, ``np.ndim(lbnd) != 0``, or
+        ``np.ndim(scl) != 0``.
+
+    See Also
+    --------
+    polyder
+
+    Notes
+    -----
+    Note that the result of each integration is *multiplied* by `scl`.  Why
+    is this important to note?  Say one is making a linear change of
+    variable :math:`u = ax + b` in an integral relative to `x`. Then
+    :math:`dx = du/a`, so one will need to set `scl` equal to
+    :math:`1/a` - perhaps not what one would have first thought.
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polynomial as P
+    >>> c = (1,2,3)
+    >>> P.polyint(c) # should return array([0, 1, 1, 1])
+    array([0.,  1.,  1.,  1.])
+    >>> P.polyint(c,3) # should return array([0, 0, 0, 1/6, 1/12, 1/20])
+     array([ 0.        ,  0.        ,  0.        ,  0.16666667,  0.08333333, # may vary
+             0.05      ])
+    >>> P.polyint(c,k=3) # should return array([3, 1, 1, 1])
+    array([3.,  1.,  1.,  1.])
+    >>> P.polyint(c,lbnd=-2) # should return array([6, 1, 1, 1])
+    array([6.,  1.,  1.,  1.])
+    >>> P.polyint(c,scl=-2) # should return array([0, -2, -2, -2])
+    array([ 0., -2., -2., -2.])
+
+    """
+    c = np.array(c, ndmin=1, copy=True)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        # astype doesn't preserve mask attribute.
+        c = c + 0.0
+    cdt = c.dtype
+    if not np.iterable(k):
+        k = [k]
+    cnt = pu._deprecate_as_int(m, "the order of integration")
+    iaxis = pu._deprecate_as_int(axis, "the axis")
+    if cnt < 0:
+        raise ValueError("The order of integration must be non-negative")
+    if len(k) > cnt:
+        raise ValueError("Too many integration constants")
+    if np.ndim(lbnd) != 0:
+        raise ValueError("lbnd must be a scalar.")
+    if np.ndim(scl) != 0:
+        raise ValueError("scl must be a scalar.")
+    iaxis = normalize_axis_index(iaxis, c.ndim)
+
+    if cnt == 0:
+        return c
+
+    k = list(k) + [0]*(cnt - len(k))
+    c = np.moveaxis(c, iaxis, 0)
+    for i in range(cnt):
+        n = len(c)
+        c *= scl
+        if n == 1 and np.all(c[0] == 0):
+            c[0] += k[i]
+        else:
+            tmp = np.empty((n + 1,) + c.shape[1:], dtype=cdt)
+            tmp[0] = c[0]*0
+            tmp[1] = c[0]
+            for j in range(1, n):
+                tmp[j + 1] = c[j]/(j + 1)
+            tmp[0] += k[i] - polyval(lbnd, tmp)
+            c = tmp
+    c = np.moveaxis(c, 0, iaxis)
+    return c
+
+
+def polyval(x, c, tensor=True):
+    """
+    Evaluate a polynomial at points x.
+
+    If `c` is of length `n + 1`, this function returns the value
+
+    .. math:: p(x) = c_0 + c_1 * x + ... + c_n * x^n
+
+    The parameter `x` is converted to an array only if it is a tuple or a
+    list, otherwise it is treated as a scalar. In either case, either `x`
+    or its elements must support multiplication and addition both with
+    themselves and with the elements of `c`.
+
+    If `c` is a 1-D array, then `p(x)` will have the same shape as `x`.  If
+    `c` is multidimensional, then the shape of the result depends on the
+    value of `tensor`. If `tensor` is true the shape will be c.shape[1:] +
+    x.shape. If `tensor` is false the shape will be c.shape[1:]. Note that
+    scalars have shape (,).
+
+    Trailing zeros in the coefficients will be used in the evaluation, so
+    they should be avoided if efficiency is a concern.
+
+    Parameters
+    ----------
+    x : array_like, compatible object
+        If `x` is a list or tuple, it is converted to an ndarray, otherwise
+        it is left unchanged and treated as a scalar. In either case, `x`
+        or its elements must support addition and multiplication with
+        with themselves and with the elements of `c`.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree n are contained in c[n]. If `c` is multidimensional the
+        remaining indices enumerate multiple polynomials. In the two
+        dimensional case the coefficients may be thought of as stored in
+        the columns of `c`.
+    tensor : boolean, optional
+        If True, the shape of the coefficient array is extended with ones
+        on the right, one for each dimension of `x`. Scalars have dimension 0
+        for this action. The result is that every column of coefficients in
+        `c` is evaluated for every element of `x`. If False, `x` is broadcast
+        over the columns of `c` for the evaluation.  This keyword is useful
+        when `c` is multidimensional. The default value is True.
+
+        .. versionadded:: 1.7.0
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The shape of the returned array is described above.
+
+    See Also
+    --------
+    polyval2d, polygrid2d, polyval3d, polygrid3d
+
+    Notes
+    -----
+    The evaluation uses Horner's method.
+
+    Examples
+    --------
+    >>> from numpy.polynomial.polynomial import polyval
+    >>> polyval(1, [1,2,3])
+    6.0
+    >>> a = np.arange(4).reshape(2,2)
+    >>> a
+    array([[0, 1],
+           [2, 3]])
+    >>> polyval(a, [1,2,3])
+    array([[ 1.,   6.],
+           [17.,  34.]])
+    >>> coef = np.arange(4).reshape(2,2) # multidimensional coefficients
+    >>> coef
+    array([[0, 1],
+           [2, 3]])
+    >>> polyval([1,2], coef, tensor=True)
+    array([[2.,  4.],
+           [4.,  7.]])
+    >>> polyval([1,2], coef, tensor=False)
+    array([2.,  7.])
+
+    """
+    c = np.array(c, ndmin=1, copy=False)
+    if c.dtype.char in '?bBhHiIlLqQpP':
+        # astype fails with NA
+        c = c + 0.0
+    if isinstance(x, (tuple, list)):
+        x = np.asarray(x)
+    if isinstance(x, np.ndarray) and tensor:
+        c = c.reshape(c.shape + (1,)*x.ndim)
+
+    c0 = c[-1] + x*0
+    for i in range(2, len(c) + 1):
+        c0 = c[-i] + c0*x
+    return c0
+
+
+def polyvalfromroots(x, r, tensor=True):
+    """
+    Evaluate a polynomial specified by its roots at points x.
+
+    If `r` is of length `N`, this function returns the value
+
+    .. math:: p(x) = \\prod_{n=1}^{N} (x - r_n)
+
+    The parameter `x` is converted to an array only if it is a tuple or a
+    list, otherwise it is treated as a scalar. In either case, either `x`
+    or its elements must support multiplication and addition both with
+    themselves and with the elements of `r`.
+
+    If `r` is a 1-D array, then `p(x)` will have the same shape as `x`.  If `r`
+    is multidimensional, then the shape of the result depends on the value of
+    `tensor`. If `tensor` is ``True`` the shape will be r.shape[1:] + x.shape;
+    that is, each polynomial is evaluated at every value of `x`. If `tensor` is
+    ``False``, the shape will be r.shape[1:]; that is, each polynomial is
+    evaluated only for the corresponding broadcast value of `x`. Note that
+    scalars have shape (,).
+
+    .. versionadded:: 1.12
+
+    Parameters
+    ----------
+    x : array_like, compatible object
+        If `x` is a list or tuple, it is converted to an ndarray, otherwise
+        it is left unchanged and treated as a scalar. In either case, `x`
+        or its elements must support addition and multiplication with
+        with themselves and with the elements of `r`.
+    r : array_like
+        Array of roots. If `r` is multidimensional the first index is the
+        root index, while the remaining indices enumerate multiple
+        polynomials. For instance, in the two dimensional case the roots
+        of each polynomial may be thought of as stored in the columns of `r`.
+    tensor : boolean, optional
+        If True, the shape of the roots array is extended with ones on the
+        right, one for each dimension of `x`. Scalars have dimension 0 for this
+        action. The result is that every column of coefficients in `r` is
+        evaluated for every element of `x`. If False, `x` is broadcast over the
+        columns of `r` for the evaluation.  This keyword is useful when `r` is
+        multidimensional. The default value is True.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The shape of the returned array is described above.
+
+    See Also
+    --------
+    polyroots, polyfromroots, polyval
+
+    Examples
+    --------
+    >>> from numpy.polynomial.polynomial import polyvalfromroots
+    >>> polyvalfromroots(1, [1,2,3])
+    0.0
+    >>> a = np.arange(4).reshape(2,2)
+    >>> a
+    array([[0, 1],
+           [2, 3]])
+    >>> polyvalfromroots(a, [-1, 0, 1])
+    array([[-0.,   0.],
+           [ 6.,  24.]])
+    >>> r = np.arange(-2, 2).reshape(2,2) # multidimensional coefficients
+    >>> r # each column of r defines one polynomial
+    array([[-2, -1],
+           [ 0,  1]])
+    >>> b = [-2, 1]
+    >>> polyvalfromroots(b, r, tensor=True)
+    array([[-0.,  3.],
+           [ 3., 0.]])
+    >>> polyvalfromroots(b, r, tensor=False)
+    array([-0.,  0.])
+    """
+    r = np.array(r, ndmin=1, copy=False)
+    if r.dtype.char in '?bBhHiIlLqQpP':
+        r = r.astype(np.double)
+    if isinstance(x, (tuple, list)):
+        x = np.asarray(x)
+    if isinstance(x, np.ndarray):
+        if tensor:
+            r = r.reshape(r.shape + (1,)*x.ndim)
+        elif x.ndim >= r.ndim:
+            raise ValueError("x.ndim must be < r.ndim when tensor == False")
+    return np.prod(x - r, axis=0)
+
+
+def polyval2d(x, y, c):
+    """
+    Evaluate a 2-D polynomial at points (x, y).
+
+    This function returns the value
+
+    .. math:: p(x,y) = \\sum_{i,j} c_{i,j} * x^i * y^j
+
+    The parameters `x` and `y` are converted to arrays only if they are
+    tuples or a lists, otherwise they are treated as a scalars and they
+    must have the same shape after conversion. In either case, either `x`
+    and `y` or their elements must support multiplication and addition both
+    with themselves and with the elements of `c`.
+
+    If `c` has fewer than two dimensions, ones are implicitly appended to
+    its shape to make it 2-D. The shape of the result will be c.shape[2:] +
+    x.shape.
+
+    Parameters
+    ----------
+    x, y : array_like, compatible objects
+        The two dimensional series is evaluated at the points `(x, y)`,
+        where `x` and `y` must have the same shape. If `x` or `y` is a list
+        or tuple, it is first converted to an ndarray, otherwise it is left
+        unchanged and, if it isn't an ndarray, it is treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term
+        of multi-degree i,j is contained in `c[i,j]`. If `c` has
+        dimension greater than two the remaining indices enumerate multiple
+        sets of coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points formed with
+        pairs of corresponding values from `x` and `y`.
+
+    See Also
+    --------
+    polyval, polygrid2d, polyval3d, polygrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._valnd(polyval, c, x, y)
+
+
+def polygrid2d(x, y, c):
+    """
+    Evaluate a 2-D polynomial on the Cartesian product of x and y.
+
+    This function returns the values:
+
+    .. math:: p(a,b) = \\sum_{i,j} c_{i,j} * a^i * b^j
+
+    where the points `(a, b)` consist of all pairs formed by taking
+    `a` from `x` and `b` from `y`. The resulting points form a grid with
+    `x` in the first dimension and `y` in the second.
+
+    The parameters `x` and `y` are converted to arrays only if they are
+    tuples or a lists, otherwise they are treated as a scalars. In either
+    case, either `x` and `y` or their elements must support multiplication
+    and addition both with themselves and with the elements of `c`.
+
+    If `c` has fewer than two dimensions, ones are implicitly appended to
+    its shape to make it 2-D. The shape of the result will be c.shape[2:] +
+    x.shape + y.shape.
+
+    Parameters
+    ----------
+    x, y : array_like, compatible objects
+        The two dimensional series is evaluated at the points in the
+        Cartesian product of `x` and `y`.  If `x` or `y` is a list or
+        tuple, it is first converted to an ndarray, otherwise it is left
+        unchanged and, if it isn't an ndarray, it is treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree i,j are contained in ``c[i,j]``. If `c` has dimension
+        greater than two the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points in the Cartesian
+        product of `x` and `y`.
+
+    See Also
+    --------
+    polyval, polyval2d, polyval3d, polygrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._gridnd(polyval, c, x, y)
+
+
+def polyval3d(x, y, z, c):
+    """
+    Evaluate a 3-D polynomial at points (x, y, z).
+
+    This function returns the values:
+
+    .. math:: p(x,y,z) = \\sum_{i,j,k} c_{i,j,k} * x^i * y^j * z^k
+
+    The parameters `x`, `y`, and `z` are converted to arrays only if
+    they are tuples or a lists, otherwise they are treated as a scalars and
+    they must have the same shape after conversion. In either case, either
+    `x`, `y`, and `z` or their elements must support multiplication and
+    addition both with themselves and with the elements of `c`.
+
+    If `c` has fewer than 3 dimensions, ones are implicitly appended to its
+    shape to make it 3-D. The shape of the result will be c.shape[3:] +
+    x.shape.
+
+    Parameters
+    ----------
+    x, y, z : array_like, compatible object
+        The three dimensional series is evaluated at the points
+        `(x, y, z)`, where `x`, `y`, and `z` must have the same shape.  If
+        any of `x`, `y`, or `z` is a list or tuple, it is first converted
+        to an ndarray, otherwise it is left unchanged and if it isn't an
+        ndarray it is  treated as a scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficient of the term of
+        multi-degree i,j,k is contained in ``c[i,j,k]``. If `c` has dimension
+        greater than 3 the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the multidimensional polynomial on points formed with
+        triples of corresponding values from `x`, `y`, and `z`.
+
+    See Also
+    --------
+    polyval, polyval2d, polygrid2d, polygrid3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._valnd(polyval, c, x, y, z)
+
+
+def polygrid3d(x, y, z, c):
+    """
+    Evaluate a 3-D polynomial on the Cartesian product of x, y and z.
+
+    This function returns the values:
+
+    .. math:: p(a,b,c) = \\sum_{i,j,k} c_{i,j,k} * a^i * b^j * c^k
+
+    where the points `(a, b, c)` consist of all triples formed by taking
+    `a` from `x`, `b` from `y`, and `c` from `z`. The resulting points form
+    a grid with `x` in the first dimension, `y` in the second, and `z` in
+    the third.
+
+    The parameters `x`, `y`, and `z` are converted to arrays only if they
+    are tuples or a lists, otherwise they are treated as a scalars. In
+    either case, either `x`, `y`, and `z` or their elements must support
+    multiplication and addition both with themselves and with the elements
+    of `c`.
+
+    If `c` has fewer than three dimensions, ones are implicitly appended to
+    its shape to make it 3-D. The shape of the result will be c.shape[3:] +
+    x.shape + y.shape + z.shape.
+
+    Parameters
+    ----------
+    x, y, z : array_like, compatible objects
+        The three dimensional series is evaluated at the points in the
+        Cartesian product of `x`, `y`, and `z`.  If `x`,`y`, or `z` is a
+        list or tuple, it is first converted to an ndarray, otherwise it is
+        left unchanged and, if it isn't an ndarray, it is treated as a
+        scalar.
+    c : array_like
+        Array of coefficients ordered so that the coefficients for terms of
+        degree i,j are contained in ``c[i,j]``. If `c` has dimension
+        greater than two the remaining indices enumerate multiple sets of
+        coefficients.
+
+    Returns
+    -------
+    values : ndarray, compatible object
+        The values of the two dimensional polynomial at points in the Cartesian
+        product of `x` and `y`.
+
+    See Also
+    --------
+    polyval, polyval2d, polygrid2d, polyval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._gridnd(polyval, c, x, y, z)
+
+
+def polyvander(x, deg):
+    """Vandermonde matrix of given degree.
+
+    Returns the Vandermonde matrix of degree `deg` and sample points
+    `x`. The Vandermonde matrix is defined by
+
+    .. math:: V[..., i] = x^i,
+
+    where `0 <= i <= deg`. The leading indices of `V` index the elements of
+    `x` and the last index is the power of `x`.
+
+    If `c` is a 1-D array of coefficients of length `n + 1` and `V` is the
+    matrix ``V = polyvander(x, n)``, then ``np.dot(V, c)`` and
+    ``polyval(x, c)`` are the same up to roundoff. This equivalence is
+    useful both for least squares fitting and for the evaluation of a large
+    number of polynomials of the same degree and sample points.
+
+    Parameters
+    ----------
+    x : array_like
+        Array of points. The dtype is converted to float64 or complex128
+        depending on whether any of the elements are complex. If `x` is
+        scalar it is converted to a 1-D array.
+    deg : int
+        Degree of the resulting matrix.
+
+    Returns
+    -------
+    vander : ndarray.
+        The Vandermonde matrix. The shape of the returned matrix is
+        ``x.shape + (deg + 1,)``, where the last index is the power of `x`.
+        The dtype will be the same as the converted `x`.
+
+    See Also
+    --------
+    polyvander2d, polyvander3d
+
+    """
+    ideg = pu._deprecate_as_int(deg, "deg")
+    if ideg < 0:
+        raise ValueError("deg must be non-negative")
+
+    x = np.array(x, copy=False, ndmin=1) + 0.0
+    dims = (ideg + 1,) + x.shape
+    dtyp = x.dtype
+    v = np.empty(dims, dtype=dtyp)
+    v[0] = x*0 + 1
+    if ideg > 0:
+        v[1] = x
+        for i in range(2, ideg + 1):
+            v[i] = v[i-1]*x
+    return np.moveaxis(v, 0, -1)
+
+
+def polyvander2d(x, y, deg):
+    """Pseudo-Vandermonde matrix of given degrees.
+
+    Returns the pseudo-Vandermonde matrix of degrees `deg` and sample
+    points `(x, y)`. The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., (deg[1] + 1)*i + j] = x^i * y^j,
+
+    where `0 <= i <= deg[0]` and `0 <= j <= deg[1]`. The leading indices of
+    `V` index the points `(x, y)` and the last index encodes the powers of
+    `x` and `y`.
+
+    If ``V = polyvander2d(x, y, [xdeg, ydeg])``, then the columns of `V`
+    correspond to the elements of a 2-D coefficient array `c` of shape
+    (xdeg + 1, ydeg + 1) in the order
+
+    .. math:: c_{00}, c_{01}, c_{02} ... , c_{10}, c_{11}, c_{12} ...
+
+    and ``np.dot(V, c.flat)`` and ``polyval2d(x, y, c)`` will be the same
+    up to roundoff. This equivalence is useful both for least squares
+    fitting and for the evaluation of a large number of 2-D polynomials
+    of the same degrees and sample points.
+
+    Parameters
+    ----------
+    x, y : array_like
+        Arrays of point coordinates, all of the same shape. The dtypes
+        will be converted to either float64 or complex128 depending on
+        whether any of the elements are complex. Scalars are converted to
+        1-D arrays.
+    deg : list of ints
+        List of maximum degrees of the form [x_deg, y_deg].
+
+    Returns
+    -------
+    vander2d : ndarray
+        The shape of the returned matrix is ``x.shape + (order,)``, where
+        :math:`order = (deg[0]+1)*(deg([1]+1)`.  The dtype will be the same
+        as the converted `x` and `y`.
+
+    See Also
+    --------
+    polyvander, polyvander3d, polyval2d, polyval3d
+
+    """
+    return pu._vander_nd_flat((polyvander, polyvander), (x, y), deg)
+
+
+def polyvander3d(x, y, z, deg):
+    """Pseudo-Vandermonde matrix of given degrees.
+
+    Returns the pseudo-Vandermonde matrix of degrees `deg` and sample
+    points `(x, y, z)`. If `l, m, n` are the given degrees in `x, y, z`,
+    then The pseudo-Vandermonde matrix is defined by
+
+    .. math:: V[..., (m+1)(n+1)i + (n+1)j + k] = x^i * y^j * z^k,
+
+    where `0 <= i <= l`, `0 <= j <= m`, and `0 <= j <= n`.  The leading
+    indices of `V` index the points `(x, y, z)` and the last index encodes
+    the powers of `x`, `y`, and `z`.
+
+    If ``V = polyvander3d(x, y, z, [xdeg, ydeg, zdeg])``, then the columns
+    of `V` correspond to the elements of a 3-D coefficient array `c` of
+    shape (xdeg + 1, ydeg + 1, zdeg + 1) in the order
+
+    .. math:: c_{000}, c_{001}, c_{002},... , c_{010}, c_{011}, c_{012},...
+
+    and  ``np.dot(V, c.flat)`` and ``polyval3d(x, y, z, c)`` will be the
+    same up to roundoff. This equivalence is useful both for least squares
+    fitting and for the evaluation of a large number of 3-D polynomials
+    of the same degrees and sample points.
+
+    Parameters
+    ----------
+    x, y, z : array_like
+        Arrays of point coordinates, all of the same shape. The dtypes will
+        be converted to either float64 or complex128 depending on whether
+        any of the elements are complex. Scalars are converted to 1-D
+        arrays.
+    deg : list of ints
+        List of maximum degrees of the form [x_deg, y_deg, z_deg].
+
+    Returns
+    -------
+    vander3d : ndarray
+        The shape of the returned matrix is ``x.shape + (order,)``, where
+        :math:`order = (deg[0]+1)*(deg([1]+1)*(deg[2]+1)`.  The dtype will
+        be the same as the converted `x`, `y`, and `z`.
+
+    See Also
+    --------
+    polyvander, polyvander3d, polyval2d, polyval3d
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    return pu._vander_nd_flat((polyvander, polyvander, polyvander), (x, y, z), deg)
+
+
+def polyfit(x, y, deg, rcond=None, full=False, w=None):
+    """
+    Least-squares fit of a polynomial to data.
+
+    Return the coefficients of a polynomial of degree `deg` that is the
+    least squares fit to the data values `y` given at points `x`. If `y` is
+    1-D the returned coefficients will also be 1-D. If `y` is 2-D multiple
+    fits are done, one for each column of `y`, and the resulting
+    coefficients are stored in the corresponding columns of a 2-D return.
+    The fitted polynomial(s) are in the form
+
+    .. math::  p(x) = c_0 + c_1 * x + ... + c_n * x^n,
+
+    where `n` is `deg`.
+
+    Parameters
+    ----------
+    x : array_like, shape (`M`,)
+        x-coordinates of the `M` sample (data) points ``(x[i], y[i])``.
+    y : array_like, shape (`M`,) or (`M`, `K`)
+        y-coordinates of the sample points.  Several sets of sample points
+        sharing the same x-coordinates can be (independently) fit with one
+        call to `polyfit` by passing in for `y` a 2-D array that contains
+        one data set per column.
+    deg : int or 1-D array_like
+        Degree(s) of the fitting polynomials. If `deg` is a single integer
+        all terms up to and including the `deg`'th term are included in the
+        fit. For NumPy versions >= 1.11.0 a list of integers specifying the
+        degrees of the terms to include may be used instead.
+    rcond : float, optional
+        Relative condition number of the fit.  Singular values smaller
+        than `rcond`, relative to the largest singular value, will be
+        ignored.  The default value is ``len(x)*eps``, where `eps` is the
+        relative precision of the platform's float type, about 2e-16 in
+        most cases.
+    full : bool, optional
+        Switch determining the nature of the return value.  When ``False``
+        (the default) just the coefficients are returned; when ``True``,
+        diagnostic information from the singular value decomposition (used
+        to solve the fit's matrix equation) is also returned.
+    w : array_like, shape (`M`,), optional
+        Weights. If not None, the weight ``w[i]`` applies to the unsquared
+        residual ``y[i] - y_hat[i]`` at ``x[i]``. Ideally the weights are
+        chosen so that the errors of the products ``w[i]*y[i]`` all have the
+        same variance.  When using inverse-variance weighting, use
+        ``w[i] = 1/sigma(y[i])``.  The default value is None.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    coef : ndarray, shape (`deg` + 1,) or (`deg` + 1, `K`)
+        Polynomial coefficients ordered from low to high.  If `y` was 2-D,
+        the coefficients in column `k` of `coef` represent the polynomial
+        fit to the data in `y`'s `k`-th column.
+
+    [residuals, rank, singular_values, rcond] : list
+        These values are only returned if ``full == True``
+
+        - residuals -- sum of squared residuals of the least squares fit
+        - rank -- the numerical rank of the scaled Vandermonde matrix
+        - singular_values -- singular values of the scaled Vandermonde matrix
+        - rcond -- value of `rcond`.
+
+        For more details, see `numpy.linalg.lstsq`.
+
+    Raises
+    ------
+    RankWarning
+        Raised if the matrix in the least-squares fit is rank deficient.
+        The warning is only raised if ``full == False``.  The warnings can
+        be turned off by:
+
+        >>> import warnings
+        >>> warnings.simplefilter('ignore', np.RankWarning)
+
+    See Also
+    --------
+    numpy.polynomial.chebyshev.chebfit
+    numpy.polynomial.legendre.legfit
+    numpy.polynomial.laguerre.lagfit
+    numpy.polynomial.hermite.hermfit
+    numpy.polynomial.hermite_e.hermefit
+    polyval : Evaluates a polynomial.
+    polyvander : Vandermonde matrix for powers.
+    numpy.linalg.lstsq : Computes a least-squares fit from the matrix.
+    scipy.interpolate.UnivariateSpline : Computes spline fits.
+
+    Notes
+    -----
+    The solution is the coefficients of the polynomial `p` that minimizes
+    the sum of the weighted squared errors
+
+    .. math:: E = \\sum_j w_j^2 * |y_j - p(x_j)|^2,
+
+    where the :math:`w_j` are the weights. This problem is solved by
+    setting up the (typically) over-determined matrix equation:
+
+    .. math:: V(x) * c = w * y,
+
+    where `V` is the weighted pseudo Vandermonde matrix of `x`, `c` are the
+    coefficients to be solved for, `w` are the weights, and `y` are the
+    observed values.  This equation is then solved using the singular value
+    decomposition of `V`.
+
+    If some of the singular values of `V` are so small that they are
+    neglected (and `full` == ``False``), a `RankWarning` will be raised.
+    This means that the coefficient values may be poorly determined.
+    Fitting to a lower order polynomial will usually get rid of the warning
+    (but may not be what you want, of course; if you have independent
+    reason(s) for choosing the degree which isn't working, you may have to:
+    a) reconsider those reasons, and/or b) reconsider the quality of your
+    data).  The `rcond` parameter can also be set to a value smaller than
+    its default, but the resulting fit may be spurious and have large
+    contributions from roundoff error.
+
+    Polynomial fits using double precision tend to "fail" at about
+    (polynomial) degree 20. Fits using Chebyshev or Legendre series are
+    generally better conditioned, but much can still depend on the
+    distribution of the sample points and the smoothness of the data.  If
+    the quality of the fit is inadequate, splines may be a good
+    alternative.
+
+    Examples
+    --------
+    >>> np.random.seed(123)
+    >>> from numpy.polynomial import polynomial as P
+    >>> x = np.linspace(-1,1,51) # x "data": [-1, -0.96, ..., 0.96, 1]
+    >>> y = x**3 - x + np.random.randn(len(x))  # x^3 - x + Gaussian noise
+    >>> c, stats = P.polyfit(x,y,3,full=True)
+    >>> np.random.seed(123)
+    >>> c # c[0], c[2] should be approx. 0, c[1] approx. -1, c[3] approx. 1
+    array([ 0.01909725, -1.30598256, -0.00577963,  1.02644286]) # may vary
+    >>> stats # note the large SSR, explaining the rather poor results
+     [array([ 38.06116253]), 4, array([ 1.38446749,  1.32119158,  0.50443316, # may vary
+              0.28853036]), 1.1324274851176597e-014]
+
+    Same thing without the added noise
+
+    >>> y = x**3 - x
+    >>> c, stats = P.polyfit(x,y,3,full=True)
+    >>> c # c[0], c[2] should be "very close to 0", c[1] ~= -1, c[3] ~= 1
+    array([-6.36925336e-18, -1.00000000e+00, -4.08053781e-16,  1.00000000e+00])
+    >>> stats # note the minuscule SSR
+    [array([  7.46346754e-31]), 4, array([ 1.38446749,  1.32119158, # may vary
+               0.50443316,  0.28853036]), 1.1324274851176597e-014]
+
+    """
+    return pu._fit(polyvander, x, y, deg, rcond, full, w)
+
+
+def polycompanion(c):
+    """
+    Return the companion matrix of c.
+
+    The companion matrix for power series cannot be made symmetric by
+    scaling the basis, so this function differs from those for the
+    orthogonal polynomials.
+
+    Parameters
+    ----------
+    c : array_like
+        1-D array of polynomial coefficients ordered from low to high
+        degree.
+
+    Returns
+    -------
+    mat : ndarray
+        Companion matrix of dimensions (deg, deg).
+
+    Notes
+    -----
+
+    .. versionadded:: 1.7.0
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    if len(c) < 2:
+        raise ValueError('Series must have maximum degree of at least 1.')
+    if len(c) == 2:
+        return np.array([[-c[0]/c[1]]])
+
+    n = len(c) - 1
+    mat = np.zeros((n, n), dtype=c.dtype)
+    bot = mat.reshape(-1)[n::n+1]
+    bot[...] = 1
+    mat[:, -1] -= c[:-1]/c[-1]
+    return mat
+
+
+def polyroots(c):
+    """
+    Compute the roots of a polynomial.
+
+    Return the roots (a.k.a. "zeros") of the polynomial
+
+    .. math:: p(x) = \\sum_i c[i] * x^i.
+
+    Parameters
+    ----------
+    c : 1-D array_like
+        1-D array of polynomial coefficients.
+
+    Returns
+    -------
+    out : ndarray
+        Array of the roots of the polynomial. If all the roots are real,
+        then `out` is also real, otherwise it is complex.
+
+    See Also
+    --------
+    numpy.polynomial.chebyshev.chebroots
+    numpy.polynomial.legendre.legroots
+    numpy.polynomial.laguerre.lagroots
+    numpy.polynomial.hermite.hermroots
+    numpy.polynomial.hermite_e.hermeroots
+
+    Notes
+    -----
+    The root estimates are obtained as the eigenvalues of the companion
+    matrix, Roots far from the origin of the complex plane may have large
+    errors due to the numerical instability of the power series for such
+    values. Roots with multiplicity greater than 1 will also show larger
+    errors as the value of the series near such points is relatively
+    insensitive to errors in the roots. Isolated roots near the origin can
+    be improved by a few iterations of Newton's method.
+
+    Examples
+    --------
+    >>> import numpy.polynomial.polynomial as poly
+    >>> poly.polyroots(poly.polyfromroots((-1,0,1)))
+    array([-1.,  0.,  1.])
+    >>> poly.polyroots(poly.polyfromroots((-1,0,1))).dtype
+    dtype('float64')
+    >>> j = complex(0,1)
+    >>> poly.polyroots(poly.polyfromroots((-j,0,j)))
+    array([  0.00000000e+00+0.j,   0.00000000e+00+1.j,   2.77555756e-17-1.j]) # may vary
+
+    """
+    # c is a trimmed copy
+    [c] = pu.as_series([c])
+    if len(c) < 2:
+        return np.array([], dtype=c.dtype)
+    if len(c) == 2:
+        return np.array([-c[0]/c[1]])
+
+    # rotated companion matrix reduces error
+    m = polycompanion(c)[::-1,::-1]
+    r = la.eigvals(m)
+    r.sort()
+    return r
+
+
+#
+# polynomial class
+#
+
+class Polynomial(ABCPolyBase):
+    """A power series class.
+
+    The Polynomial class provides the standard Python numerical methods
+    '+', '-', '*', '//', '%', 'divmod', '**', and '()' as well as the
+    attributes and methods listed in the `ABCPolyBase` documentation.
+
+    Parameters
+    ----------
+    coef : array_like
+        Polynomial coefficients in order of increasing degree, i.e.,
+        ``(1, 2, 3)`` give ``1 + 2*x + 3*x**2``.
+    domain : (2,) array_like, optional
+        Domain to use. The interval ``[domain[0], domain[1]]`` is mapped
+        to the interval ``[window[0], window[1]]`` by shifting and scaling.
+        The default value is [-1, 1].
+    window : (2,) array_like, optional
+        Window, see `domain` for its use. The default value is [-1, 1].
+
+        .. versionadded:: 1.6.0
+    symbol : str, optional
+        Symbol used to represent the independent variable in string
+        representations of the polynomial expression, e.g. for printing.
+        The symbol must be a valid Python identifier. Default value is 'x'.
+
+        .. versionadded:: 1.24
+
+    """
+    # Virtual Functions
+    _add = staticmethod(polyadd)
+    _sub = staticmethod(polysub)
+    _mul = staticmethod(polymul)
+    _div = staticmethod(polydiv)
+    _pow = staticmethod(polypow)
+    _val = staticmethod(polyval)
+    _int = staticmethod(polyint)
+    _der = staticmethod(polyder)
+    _fit = staticmethod(polyfit)
+    _line = staticmethod(polyline)
+    _roots = staticmethod(polyroots)
+    _fromroots = staticmethod(polyfromroots)
+
+    # Virtual properties
+    domain = np.array(polydomain)
+    window = np.array(polydomain)
+    basis_name = None
+
+    @classmethod
+    def _str_term_unicode(cls, i, arg_str):
+        if i == '1':
+            return f"·{arg_str}"
+        else:
+            return f"·{arg_str}{i.translate(cls._superscript_mapping)}"
+
+    @staticmethod
+    def _str_term_ascii(i, arg_str):
+        if i == '1':
+            return f" {arg_str}"
+        else:
+            return f" {arg_str}**{i}"
+
+    @staticmethod
+    def _repr_latex_term(i, arg_str, needs_parens):
+        if needs_parens:
+            arg_str = rf"\left({arg_str}\right)"
+        if i == 0:
+            return '1'
+        elif i == 1:
+            return arg_str
+        else:
+            return f"{arg_str}^{{{i}}}"
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/polynomial.pyi b/.venv/lib/python3.12/site-packages/numpy/polynomial/polynomial.pyi
new file mode 100644
index 00000000..3c87f9d2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/polynomial.pyi
@@ -0,0 +1,41 @@
+from typing import Any
+
+from numpy import ndarray, dtype, int_
+from numpy.polynomial._polybase import ABCPolyBase
+from numpy.polynomial.polyutils import trimcoef
+
+__all__: list[str]
+
+polytrim = trimcoef
+
+polydomain: ndarray[Any, dtype[int_]]
+polyzero: ndarray[Any, dtype[int_]]
+polyone: ndarray[Any, dtype[int_]]
+polyx: ndarray[Any, dtype[int_]]
+
+def polyline(off, scl): ...
+def polyfromroots(roots): ...
+def polyadd(c1, c2): ...
+def polysub(c1, c2): ...
+def polymulx(c): ...
+def polymul(c1, c2): ...
+def polydiv(c1, c2): ...
+def polypow(c, pow, maxpower=...): ...
+def polyder(c, m=..., scl=..., axis=...): ...
+def polyint(c, m=..., k=..., lbnd=..., scl=..., axis=...): ...
+def polyval(x, c, tensor=...): ...
+def polyvalfromroots(x, r, tensor=...): ...
+def polyval2d(x, y, c): ...
+def polygrid2d(x, y, c): ...
+def polyval3d(x, y, z, c): ...
+def polygrid3d(x, y, z, c): ...
+def polyvander(x, deg): ...
+def polyvander2d(x, y, deg): ...
+def polyvander3d(x, y, z, deg): ...
+def polyfit(x, y, deg, rcond=..., full=..., w=...): ...
+def polyroots(c): ...
+
+class Polynomial(ABCPolyBase):
+    domain: Any
+    window: Any
+    basis_name: Any
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/polyutils.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/polyutils.py
new file mode 100644
index 00000000..48291389
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/polyutils.py
@@ -0,0 +1,789 @@
+"""
+Utility classes and functions for the polynomial modules.
+
+This module provides: error and warning objects; a polynomial base class;
+and some routines used in both the `polynomial` and `chebyshev` modules.
+
+Warning objects
+---------------
+
+.. autosummary::
+   :toctree: generated/
+
+   RankWarning  raised in least-squares fit for rank-deficient matrix.
+
+Functions
+---------
+
+.. autosummary::
+   :toctree: generated/
+
+   as_series    convert list of array_likes into 1-D arrays of common type.
+   trimseq      remove trailing zeros.
+   trimcoef     remove small trailing coefficients.
+   getdomain    return the domain appropriate for a given set of abscissae.
+   mapdomain    maps points between domains.
+   mapparms     parameters of the linear map between domains.
+
+"""
+import operator
+import functools
+import warnings
+
+import numpy as np
+
+from numpy.core.multiarray import dragon4_positional, dragon4_scientific
+from numpy.core.umath import absolute
+
+__all__ = [
+    'RankWarning', 'as_series', 'trimseq',
+    'trimcoef', 'getdomain', 'mapdomain', 'mapparms',
+    'format_float']
+
+#
+# Warnings and Exceptions
+#
+
+class RankWarning(UserWarning):
+    """Issued by chebfit when the design matrix is rank deficient."""
+    pass
+
+#
+# Helper functions to convert inputs to 1-D arrays
+#
+def trimseq(seq):
+    """Remove small Poly series coefficients.
+
+    Parameters
+    ----------
+    seq : sequence
+        Sequence of Poly series coefficients. This routine fails for
+        empty sequences.
+
+    Returns
+    -------
+    series : sequence
+        Subsequence with trailing zeros removed. If the resulting sequence
+        would be empty, return the first element. The returned sequence may
+        or may not be a view.
+
+    Notes
+    -----
+    Do not lose the type info if the sequence contains unknown objects.
+
+    """
+    if len(seq) == 0:
+        return seq
+    else:
+        for i in range(len(seq) - 1, -1, -1):
+            if seq[i] != 0:
+                break
+        return seq[:i+1]
+
+
+def as_series(alist, trim=True):
+    """
+    Return argument as a list of 1-d arrays.
+
+    The returned list contains array(s) of dtype double, complex double, or
+    object.  A 1-d argument of shape ``(N,)`` is parsed into ``N`` arrays of
+    size one; a 2-d argument of shape ``(M,N)`` is parsed into ``M`` arrays
+    of size ``N`` (i.e., is "parsed by row"); and a higher dimensional array
+    raises a Value Error if it is not first reshaped into either a 1-d or 2-d
+    array.
+
+    Parameters
+    ----------
+    alist : array_like
+        A 1- or 2-d array_like
+    trim : boolean, optional
+        When True, trailing zeros are removed from the inputs.
+        When False, the inputs are passed through intact.
+
+    Returns
+    -------
+    [a1, a2,...] : list of 1-D arrays
+        A copy of the input data as a list of 1-d arrays.
+
+    Raises
+    ------
+    ValueError
+        Raised when `as_series` cannot convert its input to 1-d arrays, or at
+        least one of the resulting arrays is empty.
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polyutils as pu
+    >>> a = np.arange(4)
+    >>> pu.as_series(a)
+    [array([0.]), array([1.]), array([2.]), array([3.])]
+    >>> b = np.arange(6).reshape((2,3))
+    >>> pu.as_series(b)
+    [array([0., 1., 2.]), array([3., 4., 5.])]
+
+    >>> pu.as_series((1, np.arange(3), np.arange(2, dtype=np.float16)))
+    [array([1.]), array([0., 1., 2.]), array([0., 1.])]
+
+    >>> pu.as_series([2, [1.1, 0.]])
+    [array([2.]), array([1.1])]
+
+    >>> pu.as_series([2, [1.1, 0.]], trim=False)
+    [array([2.]), array([1.1, 0. ])]
+
+    """
+    arrays = [np.array(a, ndmin=1, copy=False) for a in alist]
+    if min([a.size for a in arrays]) == 0:
+        raise ValueError("Coefficient array is empty")
+    if any(a.ndim != 1 for a in arrays):
+        raise ValueError("Coefficient array is not 1-d")
+    if trim:
+        arrays = [trimseq(a) for a in arrays]
+
+    if any(a.dtype == np.dtype(object) for a in arrays):
+        ret = []
+        for a in arrays:
+            if a.dtype != np.dtype(object):
+                tmp = np.empty(len(a), dtype=np.dtype(object))
+                tmp[:] = a[:]
+                ret.append(tmp)
+            else:
+                ret.append(a.copy())
+    else:
+        try:
+            dtype = np.common_type(*arrays)
+        except Exception as e:
+            raise ValueError("Coefficient arrays have no common type") from e
+        ret = [np.array(a, copy=True, dtype=dtype) for a in arrays]
+    return ret
+
+
+def trimcoef(c, tol=0):
+    """
+    Remove "small" "trailing" coefficients from a polynomial.
+
+    "Small" means "small in absolute value" and is controlled by the
+    parameter `tol`; "trailing" means highest order coefficient(s), e.g., in
+    ``[0, 1, 1, 0, 0]`` (which represents ``0 + x + x**2 + 0*x**3 + 0*x**4``)
+    both the 3-rd and 4-th order coefficients would be "trimmed."
+
+    Parameters
+    ----------
+    c : array_like
+        1-d array of coefficients, ordered from lowest order to highest.
+    tol : number, optional
+        Trailing (i.e., highest order) elements with absolute value less
+        than or equal to `tol` (default value is zero) are removed.
+
+    Returns
+    -------
+    trimmed : ndarray
+        1-d array with trailing zeros removed.  If the resulting series
+        would be empty, a series containing a single zero is returned.
+
+    Raises
+    ------
+    ValueError
+        If `tol` < 0
+
+    See Also
+    --------
+    trimseq
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polyutils as pu
+    >>> pu.trimcoef((0,0,3,0,5,0,0))
+    array([0.,  0.,  3.,  0.,  5.])
+    >>> pu.trimcoef((0,0,1e-3,0,1e-5,0,0),1e-3) # item == tol is trimmed
+    array([0.])
+    >>> i = complex(0,1) # works for complex
+    >>> pu.trimcoef((3e-4,1e-3*(1-i),5e-4,2e-5*(1+i)), 1e-3)
+    array([0.0003+0.j   , 0.001 -0.001j])
+
+    """
+    if tol < 0:
+        raise ValueError("tol must be non-negative")
+
+    [c] = as_series([c])
+    [ind] = np.nonzero(np.abs(c) > tol)
+    if len(ind) == 0:
+        return c[:1]*0
+    else:
+        return c[:ind[-1] + 1].copy()
+
+def getdomain(x):
+    """
+    Return a domain suitable for given abscissae.
+
+    Find a domain suitable for a polynomial or Chebyshev series
+    defined at the values supplied.
+
+    Parameters
+    ----------
+    x : array_like
+        1-d array of abscissae whose domain will be determined.
+
+    Returns
+    -------
+    domain : ndarray
+        1-d array containing two values.  If the inputs are complex, then
+        the two returned points are the lower left and upper right corners
+        of the smallest rectangle (aligned with the axes) in the complex
+        plane containing the points `x`. If the inputs are real, then the
+        two points are the ends of the smallest interval containing the
+        points `x`.
+
+    See Also
+    --------
+    mapparms, mapdomain
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polyutils as pu
+    >>> points = np.arange(4)**2 - 5; points
+    array([-5, -4, -1,  4])
+    >>> pu.getdomain(points)
+    array([-5.,  4.])
+    >>> c = np.exp(complex(0,1)*np.pi*np.arange(12)/6) # unit circle
+    >>> pu.getdomain(c)
+    array([-1.-1.j,  1.+1.j])
+
+    """
+    [x] = as_series([x], trim=False)
+    if x.dtype.char in np.typecodes['Complex']:
+        rmin, rmax = x.real.min(), x.real.max()
+        imin, imax = x.imag.min(), x.imag.max()
+        return np.array((complex(rmin, imin), complex(rmax, imax)))
+    else:
+        return np.array((x.min(), x.max()))
+
+def mapparms(old, new):
+    """
+    Linear map parameters between domains.
+
+    Return the parameters of the linear map ``offset + scale*x`` that maps
+    `old` to `new` such that ``old[i] -> new[i]``, ``i = 0, 1``.
+
+    Parameters
+    ----------
+    old, new : array_like
+        Domains. Each domain must (successfully) convert to a 1-d array
+        containing precisely two values.
+
+    Returns
+    -------
+    offset, scale : scalars
+        The map ``L(x) = offset + scale*x`` maps the first domain to the
+        second.
+
+    See Also
+    --------
+    getdomain, mapdomain
+
+    Notes
+    -----
+    Also works for complex numbers, and thus can be used to calculate the
+    parameters required to map any line in the complex plane to any other
+    line therein.
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polyutils as pu
+    >>> pu.mapparms((-1,1),(-1,1))
+    (0.0, 1.0)
+    >>> pu.mapparms((1,-1),(-1,1))
+    (-0.0, -1.0)
+    >>> i = complex(0,1)
+    >>> pu.mapparms((-i,-1),(1,i))
+    ((1+1j), (1-0j))
+
+    """
+    oldlen = old[1] - old[0]
+    newlen = new[1] - new[0]
+    off = (old[1]*new[0] - old[0]*new[1])/oldlen
+    scl = newlen/oldlen
+    return off, scl
+
+def mapdomain(x, old, new):
+    """
+    Apply linear map to input points.
+
+    The linear map ``offset + scale*x`` that maps the domain `old` to
+    the domain `new` is applied to the points `x`.
+
+    Parameters
+    ----------
+    x : array_like
+        Points to be mapped. If `x` is a subtype of ndarray the subtype
+        will be preserved.
+    old, new : array_like
+        The two domains that determine the map.  Each must (successfully)
+        convert to 1-d arrays containing precisely two values.
+
+    Returns
+    -------
+    x_out : ndarray
+        Array of points of the same shape as `x`, after application of the
+        linear map between the two domains.
+
+    See Also
+    --------
+    getdomain, mapparms
+
+    Notes
+    -----
+    Effectively, this implements:
+
+    .. math::
+        x\\_out = new[0] + m(x - old[0])
+
+    where
+
+    .. math::
+        m = \\frac{new[1]-new[0]}{old[1]-old[0]}
+
+    Examples
+    --------
+    >>> from numpy.polynomial import polyutils as pu
+    >>> old_domain = (-1,1)
+    >>> new_domain = (0,2*np.pi)
+    >>> x = np.linspace(-1,1,6); x
+    array([-1. , -0.6, -0.2,  0.2,  0.6,  1. ])
+    >>> x_out = pu.mapdomain(x, old_domain, new_domain); x_out
+    array([ 0.        ,  1.25663706,  2.51327412,  3.76991118,  5.02654825, # may vary
+            6.28318531])
+    >>> x - pu.mapdomain(x_out, new_domain, old_domain)
+    array([0., 0., 0., 0., 0., 0.])
+
+    Also works for complex numbers (and thus can be used to map any line in
+    the complex plane to any other line therein).
+
+    >>> i = complex(0,1)
+    >>> old = (-1 - i, 1 + i)
+    >>> new = (-1 + i, 1 - i)
+    >>> z = np.linspace(old[0], old[1], 6); z
+    array([-1. -1.j , -0.6-0.6j, -0.2-0.2j,  0.2+0.2j,  0.6+0.6j,  1. +1.j ])
+    >>> new_z = pu.mapdomain(z, old, new); new_z
+    array([-1.0+1.j , -0.6+0.6j, -0.2+0.2j,  0.2-0.2j,  0.6-0.6j,  1.0-1.j ]) # may vary
+
+    """
+    x = np.asanyarray(x)
+    off, scl = mapparms(old, new)
+    return off + scl*x
+
+
+def _nth_slice(i, ndim):
+    sl = [np.newaxis] * ndim
+    sl[i] = slice(None)
+    return tuple(sl)
+
+
+def _vander_nd(vander_fs, points, degrees):
+    r"""
+    A generalization of the Vandermonde matrix for N dimensions
+
+    The result is built by combining the results of 1d Vandermonde matrices,
+
+    .. math::
+        W[i_0, \ldots, i_M, j_0, \ldots, j_N] = \prod_{k=0}^N{V_k(x_k)[i_0, \ldots, i_M, j_k]}
+
+    where
+
+    .. math::
+        N &= \texttt{len(points)} = \texttt{len(degrees)} = \texttt{len(vander\_fs)} \\
+        M &= \texttt{points[k].ndim} \\
+        V_k &= \texttt{vander\_fs[k]} \\
+        x_k &= \texttt{points[k]} \\
+        0 \le j_k &\le \texttt{degrees[k]}
+
+    Expanding the one-dimensional :math:`V_k` functions gives:
+
+    .. math::
+        W[i_0, \ldots, i_M, j_0, \ldots, j_N] = \prod_{k=0}^N{B_{k, j_k}(x_k[i_0, \ldots, i_M])}
+
+    where :math:`B_{k,m}` is the m'th basis of the polynomial construction used along
+    dimension :math:`k`. For a regular polynomial, :math:`B_{k, m}(x) = P_m(x) = x^m`.
+
+    Parameters
+    ----------
+    vander_fs : Sequence[function(array_like, int) -> ndarray]
+        The 1d vander function to use for each axis, such as ``polyvander``
+    points : Sequence[array_like]
+        Arrays of point coordinates, all of the same shape. The dtypes
+        will be converted to either float64 or complex128 depending on
+        whether any of the elements are complex. Scalars are converted to
+        1-D arrays.
+        This must be the same length as `vander_fs`.
+    degrees : Sequence[int]
+        The maximum degree (inclusive) to use for each axis.
+        This must be the same length as `vander_fs`.
+
+    Returns
+    -------
+    vander_nd : ndarray
+        An array of shape ``points[0].shape + tuple(d + 1 for d in degrees)``.
+    """
+    n_dims = len(vander_fs)
+    if n_dims != len(points):
+        raise ValueError(
+            f"Expected {n_dims} dimensions of sample points, got {len(points)}")
+    if n_dims != len(degrees):
+        raise ValueError(
+            f"Expected {n_dims} dimensions of degrees, got {len(degrees)}")
+    if n_dims == 0:
+        raise ValueError("Unable to guess a dtype or shape when no points are given")
+
+    # convert to the same shape and type
+    points = tuple(np.array(tuple(points), copy=False) + 0.0)
+
+    # produce the vandermonde matrix for each dimension, placing the last
+    # axis of each in an independent trailing axis of the output
+    vander_arrays = (
+        vander_fs[i](points[i], degrees[i])[(...,) + _nth_slice(i, n_dims)]
+        for i in range(n_dims)
+    )
+
+    # we checked this wasn't empty already, so no `initial` needed
+    return functools.reduce(operator.mul, vander_arrays)
+
+
+def _vander_nd_flat(vander_fs, points, degrees):
+    """
+    Like `_vander_nd`, but flattens the last ``len(degrees)`` axes into a single axis
+
+    Used to implement the public ``<type>vander<n>d`` functions.
+    """
+    v = _vander_nd(vander_fs, points, degrees)
+    return v.reshape(v.shape[:-len(degrees)] + (-1,))
+
+
+def _fromroots(line_f, mul_f, roots):
+    """
+    Helper function used to implement the ``<type>fromroots`` functions.
+
+    Parameters
+    ----------
+    line_f : function(float, float) -> ndarray
+        The ``<type>line`` function, such as ``polyline``
+    mul_f : function(array_like, array_like) -> ndarray
+        The ``<type>mul`` function, such as ``polymul``
+    roots
+        See the ``<type>fromroots`` functions for more detail
+    """
+    if len(roots) == 0:
+        return np.ones(1)
+    else:
+        [roots] = as_series([roots], trim=False)
+        roots.sort()
+        p = [line_f(-r, 1) for r in roots]
+        n = len(p)
+        while n > 1:
+            m, r = divmod(n, 2)
+            tmp = [mul_f(p[i], p[i+m]) for i in range(m)]
+            if r:
+                tmp[0] = mul_f(tmp[0], p[-1])
+            p = tmp
+            n = m
+        return p[0]
+
+
+def _valnd(val_f, c, *args):
+    """
+    Helper function used to implement the ``<type>val<n>d`` functions.
+
+    Parameters
+    ----------
+    val_f : function(array_like, array_like, tensor: bool) -> array_like
+        The ``<type>val`` function, such as ``polyval``
+    c, args
+        See the ``<type>val<n>d`` functions for more detail
+    """
+    args = [np.asanyarray(a) for a in args]
+    shape0 = args[0].shape
+    if not all((a.shape == shape0 for a in args[1:])):
+        if len(args) == 3:
+            raise ValueError('x, y, z are incompatible')
+        elif len(args) == 2:
+            raise ValueError('x, y are incompatible')
+        else:
+            raise ValueError('ordinates are incompatible')
+    it = iter(args)
+    x0 = next(it)
+
+    # use tensor on only the first
+    c = val_f(x0, c)
+    for xi in it:
+        c = val_f(xi, c, tensor=False)
+    return c
+
+
+def _gridnd(val_f, c, *args):
+    """
+    Helper function used to implement the ``<type>grid<n>d`` functions.
+
+    Parameters
+    ----------
+    val_f : function(array_like, array_like, tensor: bool) -> array_like
+        The ``<type>val`` function, such as ``polyval``
+    c, args
+        See the ``<type>grid<n>d`` functions for more detail
+    """
+    for xi in args:
+        c = val_f(xi, c)
+    return c
+
+
+def _div(mul_f, c1, c2):
+    """
+    Helper function used to implement the ``<type>div`` functions.
+
+    Implementation uses repeated subtraction of c2 multiplied by the nth basis.
+    For some polynomial types, a more efficient approach may be possible.
+
+    Parameters
+    ----------
+    mul_f : function(array_like, array_like) -> array_like
+        The ``<type>mul`` function, such as ``polymul``
+    c1, c2
+        See the ``<type>div`` functions for more detail
+    """
+    # c1, c2 are trimmed copies
+    [c1, c2] = as_series([c1, c2])
+    if c2[-1] == 0:
+        raise ZeroDivisionError()
+
+    lc1 = len(c1)
+    lc2 = len(c2)
+    if lc1 < lc2:
+        return c1[:1]*0, c1
+    elif lc2 == 1:
+        return c1/c2[-1], c1[:1]*0
+    else:
+        quo = np.empty(lc1 - lc2 + 1, dtype=c1.dtype)
+        rem = c1
+        for i in range(lc1 - lc2, - 1, -1):
+            p = mul_f([0]*i + [1], c2)
+            q = rem[-1]/p[-1]
+            rem = rem[:-1] - q*p[:-1]
+            quo[i] = q
+        return quo, trimseq(rem)
+
+
+def _add(c1, c2):
+    """ Helper function used to implement the ``<type>add`` functions. """
+    # c1, c2 are trimmed copies
+    [c1, c2] = as_series([c1, c2])
+    if len(c1) > len(c2):
+        c1[:c2.size] += c2
+        ret = c1
+    else:
+        c2[:c1.size] += c1
+        ret = c2
+    return trimseq(ret)
+
+
+def _sub(c1, c2):
+    """ Helper function used to implement the ``<type>sub`` functions. """
+    # c1, c2 are trimmed copies
+    [c1, c2] = as_series([c1, c2])
+    if len(c1) > len(c2):
+        c1[:c2.size] -= c2
+        ret = c1
+    else:
+        c2 = -c2
+        c2[:c1.size] += c1
+        ret = c2
+    return trimseq(ret)
+
+
+def _fit(vander_f, x, y, deg, rcond=None, full=False, w=None):
+    """
+    Helper function used to implement the ``<type>fit`` functions.
+
+    Parameters
+    ----------
+    vander_f : function(array_like, int) -> ndarray
+        The 1d vander function, such as ``polyvander``
+    c1, c2
+        See the ``<type>fit`` functions for more detail
+    """
+    x = np.asarray(x) + 0.0
+    y = np.asarray(y) + 0.0
+    deg = np.asarray(deg)
+
+    # check arguments.
+    if deg.ndim > 1 or deg.dtype.kind not in 'iu' or deg.size == 0:
+        raise TypeError("deg must be an int or non-empty 1-D array of int")
+    if deg.min() < 0:
+        raise ValueError("expected deg >= 0")
+    if x.ndim != 1:
+        raise TypeError("expected 1D vector for x")
+    if x.size == 0:
+        raise TypeError("expected non-empty vector for x")
+    if y.ndim < 1 or y.ndim > 2:
+        raise TypeError("expected 1D or 2D array for y")
+    if len(x) != len(y):
+        raise TypeError("expected x and y to have same length")
+
+    if deg.ndim == 0:
+        lmax = deg
+        order = lmax + 1
+        van = vander_f(x, lmax)
+    else:
+        deg = np.sort(deg)
+        lmax = deg[-1]
+        order = len(deg)
+        van = vander_f(x, lmax)[:, deg]
+
+    # set up the least squares matrices in transposed form
+    lhs = van.T
+    rhs = y.T
+    if w is not None:
+        w = np.asarray(w) + 0.0
+        if w.ndim != 1:
+            raise TypeError("expected 1D vector for w")
+        if len(x) != len(w):
+            raise TypeError("expected x and w to have same length")
+        # apply weights. Don't use inplace operations as they
+        # can cause problems with NA.
+        lhs = lhs * w
+        rhs = rhs * w
+
+    # set rcond
+    if rcond is None:
+        rcond = len(x)*np.finfo(x.dtype).eps
+
+    # Determine the norms of the design matrix columns.
+    if issubclass(lhs.dtype.type, np.complexfloating):
+        scl = np.sqrt((np.square(lhs.real) + np.square(lhs.imag)).sum(1))
+    else:
+        scl = np.sqrt(np.square(lhs).sum(1))
+    scl[scl == 0] = 1
+
+    # Solve the least squares problem.
+    c, resids, rank, s = np.linalg.lstsq(lhs.T/scl, rhs.T, rcond)
+    c = (c.T/scl).T
+
+    # Expand c to include non-fitted coefficients which are set to zero
+    if deg.ndim > 0:
+        if c.ndim == 2:
+            cc = np.zeros((lmax+1, c.shape[1]), dtype=c.dtype)
+        else:
+            cc = np.zeros(lmax+1, dtype=c.dtype)
+        cc[deg] = c
+        c = cc
+
+    # warn on rank reduction
+    if rank != order and not full:
+        msg = "The fit may be poorly conditioned"
+        warnings.warn(msg, RankWarning, stacklevel=2)
+
+    if full:
+        return c, [resids, rank, s, rcond]
+    else:
+        return c
+
+
+def _pow(mul_f, c, pow, maxpower):
+    """
+    Helper function used to implement the ``<type>pow`` functions.
+
+    Parameters
+    ----------
+    mul_f : function(array_like, array_like) -> ndarray
+        The ``<type>mul`` function, such as ``polymul``
+    c : array_like
+        1-D array of array of series coefficients
+    pow, maxpower
+        See the ``<type>pow`` functions for more detail
+    """
+    # c is a trimmed copy
+    [c] = as_series([c])
+    power = int(pow)
+    if power != pow or power < 0:
+        raise ValueError("Power must be a non-negative integer.")
+    elif maxpower is not None and power > maxpower:
+        raise ValueError("Power is too large")
+    elif power == 0:
+        return np.array([1], dtype=c.dtype)
+    elif power == 1:
+        return c
+    else:
+        # This can be made more efficient by using powers of two
+        # in the usual way.
+        prd = c
+        for i in range(2, power + 1):
+            prd = mul_f(prd, c)
+        return prd
+
+
+def _deprecate_as_int(x, desc):
+    """
+    Like `operator.index`, but emits a deprecation warning when passed a float
+
+    Parameters
+    ----------
+    x : int-like, or float with integral value
+        Value to interpret as an integer
+    desc : str
+        description to include in any error message
+
+    Raises
+    ------
+    TypeError : if x is a non-integral float or non-numeric
+    DeprecationWarning : if x is an integral float
+    """
+    try:
+        return operator.index(x)
+    except TypeError as e:
+        # Numpy 1.17.0, 2019-03-11
+        try:
+            ix = int(x)
+        except TypeError:
+            pass
+        else:
+            if ix == x:
+                warnings.warn(
+                    f"In future, this will raise TypeError, as {desc} will "
+                    "need to be an integer not just an integral float.",
+                    DeprecationWarning,
+                    stacklevel=3
+                )
+                return ix
+
+        raise TypeError(f"{desc} must be an integer") from e
+
+
+def format_float(x, parens=False):
+    if not np.issubdtype(type(x), np.floating):
+        return str(x)
+
+    opts = np.get_printoptions()
+
+    if np.isnan(x):
+        return opts['nanstr']
+    elif np.isinf(x):
+        return opts['infstr']
+
+    exp_format = False
+    if x != 0:
+        a = absolute(x)
+        if a >= 1.e8 or a < 10**min(0, -(opts['precision']-1)//2):
+            exp_format = True
+
+    trim, unique = '0', True
+    if opts['floatmode'] == 'fixed':
+        trim, unique = 'k', False
+
+    if exp_format:
+        s = dragon4_scientific(x, precision=opts['precision'],
+                               unique=unique, trim=trim, 
+                               sign=opts['sign'] == '+')
+        if parens:
+            s = '(' + s + ')'
+    else:
+        s = dragon4_positional(x, precision=opts['precision'],
+                               fractional=True,
+                               unique=unique, trim=trim,
+                               sign=opts['sign'] == '+')
+    return s
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/polyutils.pyi b/.venv/lib/python3.12/site-packages/numpy/polynomial/polyutils.pyi
new file mode 100644
index 00000000..c0bcc678
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/polyutils.pyi
@@ -0,0 +1,11 @@
+__all__: list[str]
+
+class RankWarning(UserWarning): ...
+
+def trimseq(seq): ...
+def as_series(alist, trim=...): ...
+def trimcoef(c, tol=...): ...
+def getdomain(x): ...
+def mapparms(old, new): ...
+def mapdomain(x, old, new): ...
+def format_float(x, parens=...): ...
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/setup.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/setup.py
new file mode 100644
index 00000000..b58e867a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/setup.py
@@ -0,0 +1,10 @@
+def configuration(parent_package='',top_path=None):
+    from numpy.distutils.misc_util import Configuration
+    config = Configuration('polynomial', parent_package, top_path)
+    config.add_subpackage('tests')
+    config.add_data_files('*.pyi')
+    return config
+
+if __name__ == '__main__':
+    from numpy.distutils.core import setup
+    setup(configuration=configuration)
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/__init__.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_chebyshev.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_chebyshev.py
new file mode 100644
index 00000000..2f54bebf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_chebyshev.py
@@ -0,0 +1,619 @@
+"""Tests for chebyshev module.
+
+"""
+from functools import reduce
+
+import numpy as np
+import numpy.polynomial.chebyshev as cheb
+from numpy.polynomial.polynomial import polyval
+from numpy.testing import (
+    assert_almost_equal, assert_raises, assert_equal, assert_,
+    )
+
+
+def trim(x):
+    return cheb.chebtrim(x, tol=1e-6)
+
+T0 = [1]
+T1 = [0, 1]
+T2 = [-1, 0, 2]
+T3 = [0, -3, 0, 4]
+T4 = [1, 0, -8, 0, 8]
+T5 = [0, 5, 0, -20, 0, 16]
+T6 = [-1, 0, 18, 0, -48, 0, 32]
+T7 = [0, -7, 0, 56, 0, -112, 0, 64]
+T8 = [1, 0, -32, 0, 160, 0, -256, 0, 128]
+T9 = [0, 9, 0, -120, 0, 432, 0, -576, 0, 256]
+
+Tlist = [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9]
+
+
+class TestPrivate:
+
+    def test__cseries_to_zseries(self):
+        for i in range(5):
+            inp = np.array([2] + [1]*i, np.double)
+            tgt = np.array([.5]*i + [2] + [.5]*i, np.double)
+            res = cheb._cseries_to_zseries(inp)
+            assert_equal(res, tgt)
+
+    def test__zseries_to_cseries(self):
+        for i in range(5):
+            inp = np.array([.5]*i + [2] + [.5]*i, np.double)
+            tgt = np.array([2] + [1]*i, np.double)
+            res = cheb._zseries_to_cseries(inp)
+            assert_equal(res, tgt)
+
+
+class TestConstants:
+
+    def test_chebdomain(self):
+        assert_equal(cheb.chebdomain, [-1, 1])
+
+    def test_chebzero(self):
+        assert_equal(cheb.chebzero, [0])
+
+    def test_chebone(self):
+        assert_equal(cheb.chebone, [1])
+
+    def test_chebx(self):
+        assert_equal(cheb.chebx, [0, 1])
+
+
+class TestArithmetic:
+
+    def test_chebadd(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(max(i, j) + 1)
+                tgt[i] += 1
+                tgt[j] += 1
+                res = cheb.chebadd([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_chebsub(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(max(i, j) + 1)
+                tgt[i] += 1
+                tgt[j] -= 1
+                res = cheb.chebsub([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_chebmulx(self):
+        assert_equal(cheb.chebmulx([0]), [0])
+        assert_equal(cheb.chebmulx([1]), [0, 1])
+        for i in range(1, 5):
+            ser = [0]*i + [1]
+            tgt = [0]*(i - 1) + [.5, 0, .5]
+            assert_equal(cheb.chebmulx(ser), tgt)
+
+    def test_chebmul(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(i + j + 1)
+                tgt[i + j] += .5
+                tgt[abs(i - j)] += .5
+                res = cheb.chebmul([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_chebdiv(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                ci = [0]*i + [1]
+                cj = [0]*j + [1]
+                tgt = cheb.chebadd(ci, cj)
+                quo, rem = cheb.chebdiv(tgt, ci)
+                res = cheb.chebadd(cheb.chebmul(quo, ci), rem)
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_chebpow(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                c = np.arange(i + 1)
+                tgt = reduce(cheb.chebmul, [c]*j, np.array([1]))
+                res = cheb.chebpow(c, j)
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+
+class TestEvaluation:
+    # coefficients of 1 + 2*x + 3*x**2
+    c1d = np.array([2.5, 2., 1.5])
+    c2d = np.einsum('i,j->ij', c1d, c1d)
+    c3d = np.einsum('i,j,k->ijk', c1d, c1d, c1d)
+
+    # some random values in [-1, 1)
+    x = np.random.random((3, 5))*2 - 1
+    y = polyval(x, [1., 2., 3.])
+
+    def test_chebval(self):
+        #check empty input
+        assert_equal(cheb.chebval([], [1]).size, 0)
+
+        #check normal input)
+        x = np.linspace(-1, 1)
+        y = [polyval(x, c) for c in Tlist]
+        for i in range(10):
+            msg = f"At i={i}"
+            tgt = y[i]
+            res = cheb.chebval(x, [0]*i + [1])
+            assert_almost_equal(res, tgt, err_msg=msg)
+
+        #check that shape is preserved
+        for i in range(3):
+            dims = [2]*i
+            x = np.zeros(dims)
+            assert_equal(cheb.chebval(x, [1]).shape, dims)
+            assert_equal(cheb.chebval(x, [1, 0]).shape, dims)
+            assert_equal(cheb.chebval(x, [1, 0, 0]).shape, dims)
+
+    def test_chebval2d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test exceptions
+        assert_raises(ValueError, cheb.chebval2d, x1, x2[:2], self.c2d)
+
+        #test values
+        tgt = y1*y2
+        res = cheb.chebval2d(x1, x2, self.c2d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = cheb.chebval2d(z, z, self.c2d)
+        assert_(res.shape == (2, 3))
+
+    def test_chebval3d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test exceptions
+        assert_raises(ValueError, cheb.chebval3d, x1, x2, x3[:2], self.c3d)
+
+        #test values
+        tgt = y1*y2*y3
+        res = cheb.chebval3d(x1, x2, x3, self.c3d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = cheb.chebval3d(z, z, z, self.c3d)
+        assert_(res.shape == (2, 3))
+
+    def test_chebgrid2d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test values
+        tgt = np.einsum('i,j->ij', y1, y2)
+        res = cheb.chebgrid2d(x1, x2, self.c2d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = cheb.chebgrid2d(z, z, self.c2d)
+        assert_(res.shape == (2, 3)*2)
+
+    def test_chebgrid3d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test values
+        tgt = np.einsum('i,j,k->ijk', y1, y2, y3)
+        res = cheb.chebgrid3d(x1, x2, x3, self.c3d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = cheb.chebgrid3d(z, z, z, self.c3d)
+        assert_(res.shape == (2, 3)*3)
+
+
+class TestIntegral:
+
+    def test_chebint(self):
+        # check exceptions
+        assert_raises(TypeError, cheb.chebint, [0], .5)
+        assert_raises(ValueError, cheb.chebint, [0], -1)
+        assert_raises(ValueError, cheb.chebint, [0], 1, [0, 0])
+        assert_raises(ValueError, cheb.chebint, [0], lbnd=[0])
+        assert_raises(ValueError, cheb.chebint, [0], scl=[0])
+        assert_raises(TypeError, cheb.chebint, [0], axis=.5)
+
+        # test integration of zero polynomial
+        for i in range(2, 5):
+            k = [0]*(i - 2) + [1]
+            res = cheb.chebint([0], m=i, k=k)
+            assert_almost_equal(res, [0, 1])
+
+        # check single integration with integration constant
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            tgt = [i] + [0]*i + [1/scl]
+            chebpol = cheb.poly2cheb(pol)
+            chebint = cheb.chebint(chebpol, m=1, k=[i])
+            res = cheb.cheb2poly(chebint)
+            assert_almost_equal(trim(res), trim(tgt))
+
+        # check single integration with integration constant and lbnd
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            chebpol = cheb.poly2cheb(pol)
+            chebint = cheb.chebint(chebpol, m=1, k=[i], lbnd=-1)
+            assert_almost_equal(cheb.chebval(-1, chebint), i)
+
+        # check single integration with integration constant and scaling
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            tgt = [i] + [0]*i + [2/scl]
+            chebpol = cheb.poly2cheb(pol)
+            chebint = cheb.chebint(chebpol, m=1, k=[i], scl=2)
+            res = cheb.cheb2poly(chebint)
+            assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with default k
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = cheb.chebint(tgt, m=1)
+                res = cheb.chebint(pol, m=j)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with defined k
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = cheb.chebint(tgt, m=1, k=[k])
+                res = cheb.chebint(pol, m=j, k=list(range(j)))
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with lbnd
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = cheb.chebint(tgt, m=1, k=[k], lbnd=-1)
+                res = cheb.chebint(pol, m=j, k=list(range(j)), lbnd=-1)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with scaling
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = cheb.chebint(tgt, m=1, k=[k], scl=2)
+                res = cheb.chebint(pol, m=j, k=list(range(j)), scl=2)
+                assert_almost_equal(trim(res), trim(tgt))
+
+    def test_chebint_axis(self):
+        # check that axis keyword works
+        c2d = np.random.random((3, 4))
+
+        tgt = np.vstack([cheb.chebint(c) for c in c2d.T]).T
+        res = cheb.chebint(c2d, axis=0)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([cheb.chebint(c) for c in c2d])
+        res = cheb.chebint(c2d, axis=1)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([cheb.chebint(c, k=3) for c in c2d])
+        res = cheb.chebint(c2d, k=3, axis=1)
+        assert_almost_equal(res, tgt)
+
+
+class TestDerivative:
+
+    def test_chebder(self):
+        # check exceptions
+        assert_raises(TypeError, cheb.chebder, [0], .5)
+        assert_raises(ValueError, cheb.chebder, [0], -1)
+
+        # check that zeroth derivative does nothing
+        for i in range(5):
+            tgt = [0]*i + [1]
+            res = cheb.chebder(tgt, m=0)
+            assert_equal(trim(res), trim(tgt))
+
+        # check that derivation is the inverse of integration
+        for i in range(5):
+            for j in range(2, 5):
+                tgt = [0]*i + [1]
+                res = cheb.chebder(cheb.chebint(tgt, m=j), m=j)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check derivation with scaling
+        for i in range(5):
+            for j in range(2, 5):
+                tgt = [0]*i + [1]
+                res = cheb.chebder(cheb.chebint(tgt, m=j, scl=2), m=j, scl=.5)
+                assert_almost_equal(trim(res), trim(tgt))
+
+    def test_chebder_axis(self):
+        # check that axis keyword works
+        c2d = np.random.random((3, 4))
+
+        tgt = np.vstack([cheb.chebder(c) for c in c2d.T]).T
+        res = cheb.chebder(c2d, axis=0)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([cheb.chebder(c) for c in c2d])
+        res = cheb.chebder(c2d, axis=1)
+        assert_almost_equal(res, tgt)
+
+
+class TestVander:
+    # some random values in [-1, 1)
+    x = np.random.random((3, 5))*2 - 1
+
+    def test_chebvander(self):
+        # check for 1d x
+        x = np.arange(3)
+        v = cheb.chebvander(x, 3)
+        assert_(v.shape == (3, 4))
+        for i in range(4):
+            coef = [0]*i + [1]
+            assert_almost_equal(v[..., i], cheb.chebval(x, coef))
+
+        # check for 2d x
+        x = np.array([[1, 2], [3, 4], [5, 6]])
+        v = cheb.chebvander(x, 3)
+        assert_(v.shape == (3, 2, 4))
+        for i in range(4):
+            coef = [0]*i + [1]
+            assert_almost_equal(v[..., i], cheb.chebval(x, coef))
+
+    def test_chebvander2d(self):
+        # also tests chebval2d for non-square coefficient array
+        x1, x2, x3 = self.x
+        c = np.random.random((2, 3))
+        van = cheb.chebvander2d(x1, x2, [1, 2])
+        tgt = cheb.chebval2d(x1, x2, c)
+        res = np.dot(van, c.flat)
+        assert_almost_equal(res, tgt)
+
+        # check shape
+        van = cheb.chebvander2d([x1], [x2], [1, 2])
+        assert_(van.shape == (1, 5, 6))
+
+    def test_chebvander3d(self):
+        # also tests chebval3d for non-square coefficient array
+        x1, x2, x3 = self.x
+        c = np.random.random((2, 3, 4))
+        van = cheb.chebvander3d(x1, x2, x3, [1, 2, 3])
+        tgt = cheb.chebval3d(x1, x2, x3, c)
+        res = np.dot(van, c.flat)
+        assert_almost_equal(res, tgt)
+
+        # check shape
+        van = cheb.chebvander3d([x1], [x2], [x3], [1, 2, 3])
+        assert_(van.shape == (1, 5, 24))
+
+
+class TestFitting:
+
+    def test_chebfit(self):
+        def f(x):
+            return x*(x - 1)*(x - 2)
+
+        def f2(x):
+            return x**4 + x**2 + 1
+
+        # Test exceptions
+        assert_raises(ValueError, cheb.chebfit, [1], [1], -1)
+        assert_raises(TypeError, cheb.chebfit, [[1]], [1], 0)
+        assert_raises(TypeError, cheb.chebfit, [], [1], 0)
+        assert_raises(TypeError, cheb.chebfit, [1], [[[1]]], 0)
+        assert_raises(TypeError, cheb.chebfit, [1, 2], [1], 0)
+        assert_raises(TypeError, cheb.chebfit, [1], [1, 2], 0)
+        assert_raises(TypeError, cheb.chebfit, [1], [1], 0, w=[[1]])
+        assert_raises(TypeError, cheb.chebfit, [1], [1], 0, w=[1, 1])
+        assert_raises(ValueError, cheb.chebfit, [1], [1], [-1,])
+        assert_raises(ValueError, cheb.chebfit, [1], [1], [2, -1, 6])
+        assert_raises(TypeError, cheb.chebfit, [1], [1], [])
+
+        # Test fit
+        x = np.linspace(0, 2)
+        y = f(x)
+        #
+        coef3 = cheb.chebfit(x, y, 3)
+        assert_equal(len(coef3), 4)
+        assert_almost_equal(cheb.chebval(x, coef3), y)
+        coef3 = cheb.chebfit(x, y, [0, 1, 2, 3])
+        assert_equal(len(coef3), 4)
+        assert_almost_equal(cheb.chebval(x, coef3), y)
+        #
+        coef4 = cheb.chebfit(x, y, 4)
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(cheb.chebval(x, coef4), y)
+        coef4 = cheb.chebfit(x, y, [0, 1, 2, 3, 4])
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(cheb.chebval(x, coef4), y)
+        # check things still work if deg is not in strict increasing
+        coef4 = cheb.chebfit(x, y, [2, 3, 4, 1, 0])
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(cheb.chebval(x, coef4), y)
+        #
+        coef2d = cheb.chebfit(x, np.array([y, y]).T, 3)
+        assert_almost_equal(coef2d, np.array([coef3, coef3]).T)
+        coef2d = cheb.chebfit(x, np.array([y, y]).T, [0, 1, 2, 3])
+        assert_almost_equal(coef2d, np.array([coef3, coef3]).T)
+        # test weighting
+        w = np.zeros_like(x)
+        yw = y.copy()
+        w[1::2] = 1
+        y[0::2] = 0
+        wcoef3 = cheb.chebfit(x, yw, 3, w=w)
+        assert_almost_equal(wcoef3, coef3)
+        wcoef3 = cheb.chebfit(x, yw, [0, 1, 2, 3], w=w)
+        assert_almost_equal(wcoef3, coef3)
+        #
+        wcoef2d = cheb.chebfit(x, np.array([yw, yw]).T, 3, w=w)
+        assert_almost_equal(wcoef2d, np.array([coef3, coef3]).T)
+        wcoef2d = cheb.chebfit(x, np.array([yw, yw]).T, [0, 1, 2, 3], w=w)
+        assert_almost_equal(wcoef2d, np.array([coef3, coef3]).T)
+        # test scaling with complex values x points whose square
+        # is zero when summed.
+        x = [1, 1j, -1, -1j]
+        assert_almost_equal(cheb.chebfit(x, x, 1), [0, 1])
+        assert_almost_equal(cheb.chebfit(x, x, [0, 1]), [0, 1])
+        # test fitting only even polynomials
+        x = np.linspace(-1, 1)
+        y = f2(x)
+        coef1 = cheb.chebfit(x, y, 4)
+        assert_almost_equal(cheb.chebval(x, coef1), y)
+        coef2 = cheb.chebfit(x, y, [0, 2, 4])
+        assert_almost_equal(cheb.chebval(x, coef2), y)
+        assert_almost_equal(coef1, coef2)
+
+
+class TestInterpolate:
+
+    def f(self, x):
+        return x * (x - 1) * (x - 2)
+
+    def test_raises(self):
+        assert_raises(ValueError, cheb.chebinterpolate, self.f, -1)
+        assert_raises(TypeError, cheb.chebinterpolate, self.f, 10.)
+
+    def test_dimensions(self):
+        for deg in range(1, 5):
+            assert_(cheb.chebinterpolate(self.f, deg).shape == (deg + 1,))
+
+    def test_approximation(self):
+
+        def powx(x, p):
+            return x**p
+
+        x = np.linspace(-1, 1, 10)
+        for deg in range(0, 10):
+            for p in range(0, deg + 1):
+                c = cheb.chebinterpolate(powx, deg, (p,))
+                assert_almost_equal(cheb.chebval(x, c), powx(x, p), decimal=12)
+
+
+class TestCompanion:
+
+    def test_raises(self):
+        assert_raises(ValueError, cheb.chebcompanion, [])
+        assert_raises(ValueError, cheb.chebcompanion, [1])
+
+    def test_dimensions(self):
+        for i in range(1, 5):
+            coef = [0]*i + [1]
+            assert_(cheb.chebcompanion(coef).shape == (i, i))
+
+    def test_linear_root(self):
+        assert_(cheb.chebcompanion([1, 2])[0, 0] == -.5)
+
+
+class TestGauss:
+
+    def test_100(self):
+        x, w = cheb.chebgauss(100)
+
+        # test orthogonality. Note that the results need to be normalized,
+        # otherwise the huge values that can arise from fast growing
+        # functions like Laguerre can be very confusing.
+        v = cheb.chebvander(x, 99)
+        vv = np.dot(v.T * w, v)
+        vd = 1/np.sqrt(vv.diagonal())
+        vv = vd[:, None] * vv * vd
+        assert_almost_equal(vv, np.eye(100))
+
+        # check that the integral of 1 is correct
+        tgt = np.pi
+        assert_almost_equal(w.sum(), tgt)
+
+
+class TestMisc:
+
+    def test_chebfromroots(self):
+        res = cheb.chebfromroots([])
+        assert_almost_equal(trim(res), [1])
+        for i in range(1, 5):
+            roots = np.cos(np.linspace(-np.pi, 0, 2*i + 1)[1::2])
+            tgt = [0]*i + [1]
+            res = cheb.chebfromroots(roots)*2**(i-1)
+            assert_almost_equal(trim(res), trim(tgt))
+
+    def test_chebroots(self):
+        assert_almost_equal(cheb.chebroots([1]), [])
+        assert_almost_equal(cheb.chebroots([1, 2]), [-.5])
+        for i in range(2, 5):
+            tgt = np.linspace(-1, 1, i)
+            res = cheb.chebroots(cheb.chebfromroots(tgt))
+            assert_almost_equal(trim(res), trim(tgt))
+
+    def test_chebtrim(self):
+        coef = [2, -1, 1, 0]
+
+        # Test exceptions
+        assert_raises(ValueError, cheb.chebtrim, coef, -1)
+
+        # Test results
+        assert_equal(cheb.chebtrim(coef), coef[:-1])
+        assert_equal(cheb.chebtrim(coef, 1), coef[:-3])
+        assert_equal(cheb.chebtrim(coef, 2), [0])
+
+    def test_chebline(self):
+        assert_equal(cheb.chebline(3, 4), [3, 4])
+
+    def test_cheb2poly(self):
+        for i in range(10):
+            assert_almost_equal(cheb.cheb2poly([0]*i + [1]), Tlist[i])
+
+    def test_poly2cheb(self):
+        for i in range(10):
+            assert_almost_equal(cheb.poly2cheb(Tlist[i]), [0]*i + [1])
+
+    def test_weight(self):
+        x = np.linspace(-1, 1, 11)[1:-1]
+        tgt = 1./(np.sqrt(1 + x) * np.sqrt(1 - x))
+        res = cheb.chebweight(x)
+        assert_almost_equal(res, tgt)
+
+    def test_chebpts1(self):
+        #test exceptions
+        assert_raises(ValueError, cheb.chebpts1, 1.5)
+        assert_raises(ValueError, cheb.chebpts1, 0)
+
+        #test points
+        tgt = [0]
+        assert_almost_equal(cheb.chebpts1(1), tgt)
+        tgt = [-0.70710678118654746, 0.70710678118654746]
+        assert_almost_equal(cheb.chebpts1(2), tgt)
+        tgt = [-0.86602540378443871, 0, 0.86602540378443871]
+        assert_almost_equal(cheb.chebpts1(3), tgt)
+        tgt = [-0.9238795325, -0.3826834323, 0.3826834323, 0.9238795325]
+        assert_almost_equal(cheb.chebpts1(4), tgt)
+
+    def test_chebpts2(self):
+        #test exceptions
+        assert_raises(ValueError, cheb.chebpts2, 1.5)
+        assert_raises(ValueError, cheb.chebpts2, 1)
+
+        #test points
+        tgt = [-1, 1]
+        assert_almost_equal(cheb.chebpts2(2), tgt)
+        tgt = [-1, 0, 1]
+        assert_almost_equal(cheb.chebpts2(3), tgt)
+        tgt = [-1, -0.5, .5, 1]
+        assert_almost_equal(cheb.chebpts2(4), tgt)
+        tgt = [-1.0, -0.707106781187, 0, 0.707106781187, 1.0]
+        assert_almost_equal(cheb.chebpts2(5), tgt)
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_classes.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_classes.py
new file mode 100644
index 00000000..6322062f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_classes.py
@@ -0,0 +1,600 @@
+"""Test inter-conversion of different polynomial classes.
+
+This tests the convert and cast methods of all the polynomial classes.
+
+"""
+import operator as op
+from numbers import Number
+
+import pytest
+import numpy as np
+from numpy.polynomial import (
+    Polynomial, Legendre, Chebyshev, Laguerre, Hermite, HermiteE)
+from numpy.testing import (
+    assert_almost_equal, assert_raises, assert_equal, assert_,
+    )
+from numpy.polynomial.polyutils import RankWarning
+
+#
+# fixtures
+#
+
+classes = (
+    Polynomial, Legendre, Chebyshev, Laguerre,
+    Hermite, HermiteE
+    )
+classids = tuple(cls.__name__ for cls in classes)
+
+@pytest.fixture(params=classes, ids=classids)
+def Poly(request):
+    return request.param
+
+#
+# helper functions
+#
+random = np.random.random
+
+
+def assert_poly_almost_equal(p1, p2, msg=""):
+    try:
+        assert_(np.all(p1.domain == p2.domain))
+        assert_(np.all(p1.window == p2.window))
+        assert_almost_equal(p1.coef, p2.coef)
+    except AssertionError:
+        msg = f"Result: {p1}\nTarget: {p2}"
+        raise AssertionError(msg)
+
+
+#
+# Test conversion methods that depend on combinations of two classes.
+#
+
+Poly1 = Poly
+Poly2 = Poly
+
+
+def test_conversion(Poly1, Poly2):
+    x = np.linspace(0, 1, 10)
+    coef = random((3,))
+
+    d1 = Poly1.domain + random((2,))*.25
+    w1 = Poly1.window + random((2,))*.25
+    p1 = Poly1(coef, domain=d1, window=w1)
+
+    d2 = Poly2.domain + random((2,))*.25
+    w2 = Poly2.window + random((2,))*.25
+    p2 = p1.convert(kind=Poly2, domain=d2, window=w2)
+
+    assert_almost_equal(p2.domain, d2)
+    assert_almost_equal(p2.window, w2)
+    assert_almost_equal(p2(x), p1(x))
+
+
+def test_cast(Poly1, Poly2):
+    x = np.linspace(0, 1, 10)
+    coef = random((3,))
+
+    d1 = Poly1.domain + random((2,))*.25
+    w1 = Poly1.window + random((2,))*.25
+    p1 = Poly1(coef, domain=d1, window=w1)
+
+    d2 = Poly2.domain + random((2,))*.25
+    w2 = Poly2.window + random((2,))*.25
+    p2 = Poly2.cast(p1, domain=d2, window=w2)
+
+    assert_almost_equal(p2.domain, d2)
+    assert_almost_equal(p2.window, w2)
+    assert_almost_equal(p2(x), p1(x))
+
+
+#
+# test methods that depend on one class
+#
+
+
+def test_identity(Poly):
+    d = Poly.domain + random((2,))*.25
+    w = Poly.window + random((2,))*.25
+    x = np.linspace(d[0], d[1], 11)
+    p = Poly.identity(domain=d, window=w)
+    assert_equal(p.domain, d)
+    assert_equal(p.window, w)
+    assert_almost_equal(p(x), x)
+
+
+def test_basis(Poly):
+    d = Poly.domain + random((2,))*.25
+    w = Poly.window + random((2,))*.25
+    p = Poly.basis(5, domain=d, window=w)
+    assert_equal(p.domain, d)
+    assert_equal(p.window, w)
+    assert_equal(p.coef, [0]*5 + [1])
+
+
+def test_fromroots(Poly):
+    # check that requested roots are zeros of a polynomial
+    # of correct degree, domain, and window.
+    d = Poly.domain + random((2,))*.25
+    w = Poly.window + random((2,))*.25
+    r = random((5,))
+    p1 = Poly.fromroots(r, domain=d, window=w)
+    assert_equal(p1.degree(), len(r))
+    assert_equal(p1.domain, d)
+    assert_equal(p1.window, w)
+    assert_almost_equal(p1(r), 0)
+
+    # check that polynomial is monic
+    pdom = Polynomial.domain
+    pwin = Polynomial.window
+    p2 = Polynomial.cast(p1, domain=pdom, window=pwin)
+    assert_almost_equal(p2.coef[-1], 1)
+
+
+def test_bad_conditioned_fit(Poly):
+
+    x = [0., 0., 1.]
+    y = [1., 2., 3.]
+
+    # check RankWarning is raised
+    with pytest.warns(RankWarning) as record:
+        Poly.fit(x, y, 2)
+    assert record[0].message.args[0] == "The fit may be poorly conditioned"
+
+
+def test_fit(Poly):
+
+    def f(x):
+        return x*(x - 1)*(x - 2)
+    x = np.linspace(0, 3)
+    y = f(x)
+
+    # check default value of domain and window
+    p = Poly.fit(x, y, 3)
+    assert_almost_equal(p.domain, [0, 3])
+    assert_almost_equal(p(x), y)
+    assert_equal(p.degree(), 3)
+
+    # check with given domains and window
+    d = Poly.domain + random((2,))*.25
+    w = Poly.window + random((2,))*.25
+    p = Poly.fit(x, y, 3, domain=d, window=w)
+    assert_almost_equal(p(x), y)
+    assert_almost_equal(p.domain, d)
+    assert_almost_equal(p.window, w)
+    p = Poly.fit(x, y, [0, 1, 2, 3], domain=d, window=w)
+    assert_almost_equal(p(x), y)
+    assert_almost_equal(p.domain, d)
+    assert_almost_equal(p.window, w)
+
+    # check with class domain default
+    p = Poly.fit(x, y, 3, [])
+    assert_equal(p.domain, Poly.domain)
+    assert_equal(p.window, Poly.window)
+    p = Poly.fit(x, y, [0, 1, 2, 3], [])
+    assert_equal(p.domain, Poly.domain)
+    assert_equal(p.window, Poly.window)
+
+    # check that fit accepts weights.
+    w = np.zeros_like(x)
+    z = y + random(y.shape)*.25
+    w[::2] = 1
+    p1 = Poly.fit(x[::2], z[::2], 3)
+    p2 = Poly.fit(x, z, 3, w=w)
+    p3 = Poly.fit(x, z, [0, 1, 2, 3], w=w)
+    assert_almost_equal(p1(x), p2(x))
+    assert_almost_equal(p2(x), p3(x))
+
+
+def test_equal(Poly):
+    p1 = Poly([1, 2, 3], domain=[0, 1], window=[2, 3])
+    p2 = Poly([1, 1, 1], domain=[0, 1], window=[2, 3])
+    p3 = Poly([1, 2, 3], domain=[1, 2], window=[2, 3])
+    p4 = Poly([1, 2, 3], domain=[0, 1], window=[1, 2])
+    assert_(p1 == p1)
+    assert_(not p1 == p2)
+    assert_(not p1 == p3)
+    assert_(not p1 == p4)
+
+
+def test_not_equal(Poly):
+    p1 = Poly([1, 2, 3], domain=[0, 1], window=[2, 3])
+    p2 = Poly([1, 1, 1], domain=[0, 1], window=[2, 3])
+    p3 = Poly([1, 2, 3], domain=[1, 2], window=[2, 3])
+    p4 = Poly([1, 2, 3], domain=[0, 1], window=[1, 2])
+    assert_(not p1 != p1)
+    assert_(p1 != p2)
+    assert_(p1 != p3)
+    assert_(p1 != p4)
+
+
+def test_add(Poly):
+    # This checks commutation, not numerical correctness
+    c1 = list(random((4,)) + .5)
+    c2 = list(random((3,)) + .5)
+    p1 = Poly(c1)
+    p2 = Poly(c2)
+    p3 = p1 + p2
+    assert_poly_almost_equal(p2 + p1, p3)
+    assert_poly_almost_equal(p1 + c2, p3)
+    assert_poly_almost_equal(c2 + p1, p3)
+    assert_poly_almost_equal(p1 + tuple(c2), p3)
+    assert_poly_almost_equal(tuple(c2) + p1, p3)
+    assert_poly_almost_equal(p1 + np.array(c2), p3)
+    assert_poly_almost_equal(np.array(c2) + p1, p3)
+    assert_raises(TypeError, op.add, p1, Poly([0], domain=Poly.domain + 1))
+    assert_raises(TypeError, op.add, p1, Poly([0], window=Poly.window + 1))
+    if Poly is Polynomial:
+        assert_raises(TypeError, op.add, p1, Chebyshev([0]))
+    else:
+        assert_raises(TypeError, op.add, p1, Polynomial([0]))
+
+
+def test_sub(Poly):
+    # This checks commutation, not numerical correctness
+    c1 = list(random((4,)) + .5)
+    c2 = list(random((3,)) + .5)
+    p1 = Poly(c1)
+    p2 = Poly(c2)
+    p3 = p1 - p2
+    assert_poly_almost_equal(p2 - p1, -p3)
+    assert_poly_almost_equal(p1 - c2, p3)
+    assert_poly_almost_equal(c2 - p1, -p3)
+    assert_poly_almost_equal(p1 - tuple(c2), p3)
+    assert_poly_almost_equal(tuple(c2) - p1, -p3)
+    assert_poly_almost_equal(p1 - np.array(c2), p3)
+    assert_poly_almost_equal(np.array(c2) - p1, -p3)
+    assert_raises(TypeError, op.sub, p1, Poly([0], domain=Poly.domain + 1))
+    assert_raises(TypeError, op.sub, p1, Poly([0], window=Poly.window + 1))
+    if Poly is Polynomial:
+        assert_raises(TypeError, op.sub, p1, Chebyshev([0]))
+    else:
+        assert_raises(TypeError, op.sub, p1, Polynomial([0]))
+
+
+def test_mul(Poly):
+    c1 = list(random((4,)) + .5)
+    c2 = list(random((3,)) + .5)
+    p1 = Poly(c1)
+    p2 = Poly(c2)
+    p3 = p1 * p2
+    assert_poly_almost_equal(p2 * p1, p3)
+    assert_poly_almost_equal(p1 * c2, p3)
+    assert_poly_almost_equal(c2 * p1, p3)
+    assert_poly_almost_equal(p1 * tuple(c2), p3)
+    assert_poly_almost_equal(tuple(c2) * p1, p3)
+    assert_poly_almost_equal(p1 * np.array(c2), p3)
+    assert_poly_almost_equal(np.array(c2) * p1, p3)
+    assert_poly_almost_equal(p1 * 2, p1 * Poly([2]))
+    assert_poly_almost_equal(2 * p1, p1 * Poly([2]))
+    assert_raises(TypeError, op.mul, p1, Poly([0], domain=Poly.domain + 1))
+    assert_raises(TypeError, op.mul, p1, Poly([0], window=Poly.window + 1))
+    if Poly is Polynomial:
+        assert_raises(TypeError, op.mul, p1, Chebyshev([0]))
+    else:
+        assert_raises(TypeError, op.mul, p1, Polynomial([0]))
+
+
+def test_floordiv(Poly):
+    c1 = list(random((4,)) + .5)
+    c2 = list(random((3,)) + .5)
+    c3 = list(random((2,)) + .5)
+    p1 = Poly(c1)
+    p2 = Poly(c2)
+    p3 = Poly(c3)
+    p4 = p1 * p2 + p3
+    c4 = list(p4.coef)
+    assert_poly_almost_equal(p4 // p2, p1)
+    assert_poly_almost_equal(p4 // c2, p1)
+    assert_poly_almost_equal(c4 // p2, p1)
+    assert_poly_almost_equal(p4 // tuple(c2), p1)
+    assert_poly_almost_equal(tuple(c4) // p2, p1)
+    assert_poly_almost_equal(p4 // np.array(c2), p1)
+    assert_poly_almost_equal(np.array(c4) // p2, p1)
+    assert_poly_almost_equal(2 // p2, Poly([0]))
+    assert_poly_almost_equal(p2 // 2, 0.5*p2)
+    assert_raises(
+        TypeError, op.floordiv, p1, Poly([0], domain=Poly.domain + 1))
+    assert_raises(
+        TypeError, op.floordiv, p1, Poly([0], window=Poly.window + 1))
+    if Poly is Polynomial:
+        assert_raises(TypeError, op.floordiv, p1, Chebyshev([0]))
+    else:
+        assert_raises(TypeError, op.floordiv, p1, Polynomial([0]))
+
+
+def test_truediv(Poly):
+    # true division is valid only if the denominator is a Number and
+    # not a python bool.
+    p1 = Poly([1,2,3])
+    p2 = p1 * 5
+
+    for stype in np.ScalarType:
+        if not issubclass(stype, Number) or issubclass(stype, bool):
+            continue
+        s = stype(5)
+        assert_poly_almost_equal(op.truediv(p2, s), p1)
+        assert_raises(TypeError, op.truediv, s, p2)
+    for stype in (int, float):
+        s = stype(5)
+        assert_poly_almost_equal(op.truediv(p2, s), p1)
+        assert_raises(TypeError, op.truediv, s, p2)
+    for stype in [complex]:
+        s = stype(5, 0)
+        assert_poly_almost_equal(op.truediv(p2, s), p1)
+        assert_raises(TypeError, op.truediv, s, p2)
+    for s in [tuple(), list(), dict(), bool(), np.array([1])]:
+        assert_raises(TypeError, op.truediv, p2, s)
+        assert_raises(TypeError, op.truediv, s, p2)
+    for ptype in classes:
+        assert_raises(TypeError, op.truediv, p2, ptype(1))
+
+
+def test_mod(Poly):
+    # This checks commutation, not numerical correctness
+    c1 = list(random((4,)) + .5)
+    c2 = list(random((3,)) + .5)
+    c3 = list(random((2,)) + .5)
+    p1 = Poly(c1)
+    p2 = Poly(c2)
+    p3 = Poly(c3)
+    p4 = p1 * p2 + p3
+    c4 = list(p4.coef)
+    assert_poly_almost_equal(p4 % p2, p3)
+    assert_poly_almost_equal(p4 % c2, p3)
+    assert_poly_almost_equal(c4 % p2, p3)
+    assert_poly_almost_equal(p4 % tuple(c2), p3)
+    assert_poly_almost_equal(tuple(c4) % p2, p3)
+    assert_poly_almost_equal(p4 % np.array(c2), p3)
+    assert_poly_almost_equal(np.array(c4) % p2, p3)
+    assert_poly_almost_equal(2 % p2, Poly([2]))
+    assert_poly_almost_equal(p2 % 2, Poly([0]))
+    assert_raises(TypeError, op.mod, p1, Poly([0], domain=Poly.domain + 1))
+    assert_raises(TypeError, op.mod, p1, Poly([0], window=Poly.window + 1))
+    if Poly is Polynomial:
+        assert_raises(TypeError, op.mod, p1, Chebyshev([0]))
+    else:
+        assert_raises(TypeError, op.mod, p1, Polynomial([0]))
+
+
+def test_divmod(Poly):
+    # This checks commutation, not numerical correctness
+    c1 = list(random((4,)) + .5)
+    c2 = list(random((3,)) + .5)
+    c3 = list(random((2,)) + .5)
+    p1 = Poly(c1)
+    p2 = Poly(c2)
+    p3 = Poly(c3)
+    p4 = p1 * p2 + p3
+    c4 = list(p4.coef)
+    quo, rem = divmod(p4, p2)
+    assert_poly_almost_equal(quo, p1)
+    assert_poly_almost_equal(rem, p3)
+    quo, rem = divmod(p4, c2)
+    assert_poly_almost_equal(quo, p1)
+    assert_poly_almost_equal(rem, p3)
+    quo, rem = divmod(c4, p2)
+    assert_poly_almost_equal(quo, p1)
+    assert_poly_almost_equal(rem, p3)
+    quo, rem = divmod(p4, tuple(c2))
+    assert_poly_almost_equal(quo, p1)
+    assert_poly_almost_equal(rem, p3)
+    quo, rem = divmod(tuple(c4), p2)
+    assert_poly_almost_equal(quo, p1)
+    assert_poly_almost_equal(rem, p3)
+    quo, rem = divmod(p4, np.array(c2))
+    assert_poly_almost_equal(quo, p1)
+    assert_poly_almost_equal(rem, p3)
+    quo, rem = divmod(np.array(c4), p2)
+    assert_poly_almost_equal(quo, p1)
+    assert_poly_almost_equal(rem, p3)
+    quo, rem = divmod(p2, 2)
+    assert_poly_almost_equal(quo, 0.5*p2)
+    assert_poly_almost_equal(rem, Poly([0]))
+    quo, rem = divmod(2, p2)
+    assert_poly_almost_equal(quo, Poly([0]))
+    assert_poly_almost_equal(rem, Poly([2]))
+    assert_raises(TypeError, divmod, p1, Poly([0], domain=Poly.domain + 1))
+    assert_raises(TypeError, divmod, p1, Poly([0], window=Poly.window + 1))
+    if Poly is Polynomial:
+        assert_raises(TypeError, divmod, p1, Chebyshev([0]))
+    else:
+        assert_raises(TypeError, divmod, p1, Polynomial([0]))
+
+
+def test_roots(Poly):
+    d = Poly.domain * 1.25 + .25
+    w = Poly.window
+    tgt = np.linspace(d[0], d[1], 5)
+    res = np.sort(Poly.fromroots(tgt, domain=d, window=w).roots())
+    assert_almost_equal(res, tgt)
+    # default domain and window
+    res = np.sort(Poly.fromroots(tgt).roots())
+    assert_almost_equal(res, tgt)
+
+
+def test_degree(Poly):
+    p = Poly.basis(5)
+    assert_equal(p.degree(), 5)
+
+
+def test_copy(Poly):
+    p1 = Poly.basis(5)
+    p2 = p1.copy()
+    assert_(p1 == p2)
+    assert_(p1 is not p2)
+    assert_(p1.coef is not p2.coef)
+    assert_(p1.domain is not p2.domain)
+    assert_(p1.window is not p2.window)
+
+
+def test_integ(Poly):
+    P = Polynomial
+    # Check defaults
+    p0 = Poly.cast(P([1*2, 2*3, 3*4]))
+    p1 = P.cast(p0.integ())
+    p2 = P.cast(p0.integ(2))
+    assert_poly_almost_equal(p1, P([0, 2, 3, 4]))
+    assert_poly_almost_equal(p2, P([0, 0, 1, 1, 1]))
+    # Check with k
+    p0 = Poly.cast(P([1*2, 2*3, 3*4]))
+    p1 = P.cast(p0.integ(k=1))
+    p2 = P.cast(p0.integ(2, k=[1, 1]))
+    assert_poly_almost_equal(p1, P([1, 2, 3, 4]))
+    assert_poly_almost_equal(p2, P([1, 1, 1, 1, 1]))
+    # Check with lbnd
+    p0 = Poly.cast(P([1*2, 2*3, 3*4]))
+    p1 = P.cast(p0.integ(lbnd=1))
+    p2 = P.cast(p0.integ(2, lbnd=1))
+    assert_poly_almost_equal(p1, P([-9, 2, 3, 4]))
+    assert_poly_almost_equal(p2, P([6, -9, 1, 1, 1]))
+    # Check scaling
+    d = 2*Poly.domain
+    p0 = Poly.cast(P([1*2, 2*3, 3*4]), domain=d)
+    p1 = P.cast(p0.integ())
+    p2 = P.cast(p0.integ(2))
+    assert_poly_almost_equal(p1, P([0, 2, 3, 4]))
+    assert_poly_almost_equal(p2, P([0, 0, 1, 1, 1]))
+
+
+def test_deriv(Poly):
+    # Check that the derivative is the inverse of integration. It is
+    # assumes that the integration has been checked elsewhere.
+    d = Poly.domain + random((2,))*.25
+    w = Poly.window + random((2,))*.25
+    p1 = Poly([1, 2, 3], domain=d, window=w)
+    p2 = p1.integ(2, k=[1, 2])
+    p3 = p1.integ(1, k=[1])
+    assert_almost_equal(p2.deriv(1).coef, p3.coef)
+    assert_almost_equal(p2.deriv(2).coef, p1.coef)
+    # default domain and window
+    p1 = Poly([1, 2, 3])
+    p2 = p1.integ(2, k=[1, 2])
+    p3 = p1.integ(1, k=[1])
+    assert_almost_equal(p2.deriv(1).coef, p3.coef)
+    assert_almost_equal(p2.deriv(2).coef, p1.coef)
+
+
+def test_linspace(Poly):
+    d = Poly.domain + random((2,))*.25
+    w = Poly.window + random((2,))*.25
+    p = Poly([1, 2, 3], domain=d, window=w)
+    # check default domain
+    xtgt = np.linspace(d[0], d[1], 20)
+    ytgt = p(xtgt)
+    xres, yres = p.linspace(20)
+    assert_almost_equal(xres, xtgt)
+    assert_almost_equal(yres, ytgt)
+    # check specified domain
+    xtgt = np.linspace(0, 2, 20)
+    ytgt = p(xtgt)
+    xres, yres = p.linspace(20, domain=[0, 2])
+    assert_almost_equal(xres, xtgt)
+    assert_almost_equal(yres, ytgt)
+
+
+def test_pow(Poly):
+    d = Poly.domain + random((2,))*.25
+    w = Poly.window + random((2,))*.25
+    tgt = Poly([1], domain=d, window=w)
+    tst = Poly([1, 2, 3], domain=d, window=w)
+    for i in range(5):
+        assert_poly_almost_equal(tst**i, tgt)
+        tgt = tgt * tst
+    # default domain and window
+    tgt = Poly([1])
+    tst = Poly([1, 2, 3])
+    for i in range(5):
+        assert_poly_almost_equal(tst**i, tgt)
+        tgt = tgt * tst
+    # check error for invalid powers
+    assert_raises(ValueError, op.pow, tgt, 1.5)
+    assert_raises(ValueError, op.pow, tgt, -1)
+
+
+def test_call(Poly):
+    P = Polynomial
+    d = Poly.domain
+    x = np.linspace(d[0], d[1], 11)
+
+    # Check defaults
+    p = Poly.cast(P([1, 2, 3]))
+    tgt = 1 + x*(2 + 3*x)
+    res = p(x)
+    assert_almost_equal(res, tgt)
+
+
+def test_cutdeg(Poly):
+    p = Poly([1, 2, 3])
+    assert_raises(ValueError, p.cutdeg, .5)
+    assert_raises(ValueError, p.cutdeg, -1)
+    assert_equal(len(p.cutdeg(3)), 3)
+    assert_equal(len(p.cutdeg(2)), 3)
+    assert_equal(len(p.cutdeg(1)), 2)
+    assert_equal(len(p.cutdeg(0)), 1)
+
+
+def test_truncate(Poly):
+    p = Poly([1, 2, 3])
+    assert_raises(ValueError, p.truncate, .5)
+    assert_raises(ValueError, p.truncate, 0)
+    assert_equal(len(p.truncate(4)), 3)
+    assert_equal(len(p.truncate(3)), 3)
+    assert_equal(len(p.truncate(2)), 2)
+    assert_equal(len(p.truncate(1)), 1)
+
+
+def test_trim(Poly):
+    c = [1, 1e-6, 1e-12, 0]
+    p = Poly(c)
+    assert_equal(p.trim().coef, c[:3])
+    assert_equal(p.trim(1e-10).coef, c[:2])
+    assert_equal(p.trim(1e-5).coef, c[:1])
+
+
+def test_mapparms(Poly):
+    # check with defaults. Should be identity.
+    d = Poly.domain
+    w = Poly.window
+    p = Poly([1], domain=d, window=w)
+    assert_almost_equal([0, 1], p.mapparms())
+    #
+    w = 2*d + 1
+    p = Poly([1], domain=d, window=w)
+    assert_almost_equal([1, 2], p.mapparms())
+
+
+def test_ufunc_override(Poly):
+    p = Poly([1, 2, 3])
+    x = np.ones(3)
+    assert_raises(TypeError, np.add, p, x)
+    assert_raises(TypeError, np.add, x, p)
+
+
+#
+# Test class method that only exists for some classes
+#
+
+
+class TestInterpolate:
+
+    def f(self, x):
+        return x * (x - 1) * (x - 2)
+
+    def test_raises(self):
+        assert_raises(ValueError, Chebyshev.interpolate, self.f, -1)
+        assert_raises(TypeError, Chebyshev.interpolate, self.f, 10.)
+
+    def test_dimensions(self):
+        for deg in range(1, 5):
+            assert_(Chebyshev.interpolate(self.f, deg).degree() == deg)
+
+    def test_approximation(self):
+
+        def powx(x, p):
+            return x**p
+
+        x = np.linspace(0, 2, 10)
+        for deg in range(0, 10):
+            for t in range(0, deg + 1):
+                p = Chebyshev.interpolate(powx, deg, domain=[0, 2], args=(t,))
+                assert_almost_equal(p(x), powx(x, t), decimal=11)
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_hermite.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_hermite.py
new file mode 100644
index 00000000..53ee0844
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_hermite.py
@@ -0,0 +1,555 @@
+"""Tests for hermite module.
+
+"""
+from functools import reduce
+
+import numpy as np
+import numpy.polynomial.hermite as herm
+from numpy.polynomial.polynomial import polyval
+from numpy.testing import (
+    assert_almost_equal, assert_raises, assert_equal, assert_,
+    )
+
+H0 = np.array([1])
+H1 = np.array([0, 2])
+H2 = np.array([-2, 0, 4])
+H3 = np.array([0, -12, 0, 8])
+H4 = np.array([12, 0, -48, 0, 16])
+H5 = np.array([0, 120, 0, -160, 0, 32])
+H6 = np.array([-120, 0, 720, 0, -480, 0, 64])
+H7 = np.array([0, -1680, 0, 3360, 0, -1344, 0, 128])
+H8 = np.array([1680, 0, -13440, 0, 13440, 0, -3584, 0, 256])
+H9 = np.array([0, 30240, 0, -80640, 0, 48384, 0, -9216, 0, 512])
+
+Hlist = [H0, H1, H2, H3, H4, H5, H6, H7, H8, H9]
+
+
+def trim(x):
+    return herm.hermtrim(x, tol=1e-6)
+
+
+class TestConstants:
+
+    def test_hermdomain(self):
+        assert_equal(herm.hermdomain, [-1, 1])
+
+    def test_hermzero(self):
+        assert_equal(herm.hermzero, [0])
+
+    def test_hermone(self):
+        assert_equal(herm.hermone, [1])
+
+    def test_hermx(self):
+        assert_equal(herm.hermx, [0, .5])
+
+
+class TestArithmetic:
+    x = np.linspace(-3, 3, 100)
+
+    def test_hermadd(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(max(i, j) + 1)
+                tgt[i] += 1
+                tgt[j] += 1
+                res = herm.hermadd([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_hermsub(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(max(i, j) + 1)
+                tgt[i] += 1
+                tgt[j] -= 1
+                res = herm.hermsub([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_hermmulx(self):
+        assert_equal(herm.hermmulx([0]), [0])
+        assert_equal(herm.hermmulx([1]), [0, .5])
+        for i in range(1, 5):
+            ser = [0]*i + [1]
+            tgt = [0]*(i - 1) + [i, 0, .5]
+            assert_equal(herm.hermmulx(ser), tgt)
+
+    def test_hermmul(self):
+        # check values of result
+        for i in range(5):
+            pol1 = [0]*i + [1]
+            val1 = herm.hermval(self.x, pol1)
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                pol2 = [0]*j + [1]
+                val2 = herm.hermval(self.x, pol2)
+                pol3 = herm.hermmul(pol1, pol2)
+                val3 = herm.hermval(self.x, pol3)
+                assert_(len(pol3) == i + j + 1, msg)
+                assert_almost_equal(val3, val1*val2, err_msg=msg)
+
+    def test_hermdiv(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                ci = [0]*i + [1]
+                cj = [0]*j + [1]
+                tgt = herm.hermadd(ci, cj)
+                quo, rem = herm.hermdiv(tgt, ci)
+                res = herm.hermadd(herm.hermmul(quo, ci), rem)
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_hermpow(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                c = np.arange(i + 1)
+                tgt = reduce(herm.hermmul, [c]*j, np.array([1]))
+                res = herm.hermpow(c, j) 
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+
+class TestEvaluation:
+    # coefficients of 1 + 2*x + 3*x**2
+    c1d = np.array([2.5, 1., .75])
+    c2d = np.einsum('i,j->ij', c1d, c1d)
+    c3d = np.einsum('i,j,k->ijk', c1d, c1d, c1d)
+
+    # some random values in [-1, 1)
+    x = np.random.random((3, 5))*2 - 1
+    y = polyval(x, [1., 2., 3.])
+
+    def test_hermval(self):
+        #check empty input
+        assert_equal(herm.hermval([], [1]).size, 0)
+
+        #check normal input)
+        x = np.linspace(-1, 1)
+        y = [polyval(x, c) for c in Hlist]
+        for i in range(10):
+            msg = f"At i={i}"
+            tgt = y[i]
+            res = herm.hermval(x, [0]*i + [1])
+            assert_almost_equal(res, tgt, err_msg=msg)
+
+        #check that shape is preserved
+        for i in range(3):
+            dims = [2]*i
+            x = np.zeros(dims)
+            assert_equal(herm.hermval(x, [1]).shape, dims)
+            assert_equal(herm.hermval(x, [1, 0]).shape, dims)
+            assert_equal(herm.hermval(x, [1, 0, 0]).shape, dims)
+
+    def test_hermval2d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test exceptions
+        assert_raises(ValueError, herm.hermval2d, x1, x2[:2], self.c2d)
+
+        #test values
+        tgt = y1*y2
+        res = herm.hermval2d(x1, x2, self.c2d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = herm.hermval2d(z, z, self.c2d)
+        assert_(res.shape == (2, 3))
+
+    def test_hermval3d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test exceptions
+        assert_raises(ValueError, herm.hermval3d, x1, x2, x3[:2], self.c3d)
+
+        #test values
+        tgt = y1*y2*y3
+        res = herm.hermval3d(x1, x2, x3, self.c3d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = herm.hermval3d(z, z, z, self.c3d)
+        assert_(res.shape == (2, 3))
+
+    def test_hermgrid2d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test values
+        tgt = np.einsum('i,j->ij', y1, y2)
+        res = herm.hermgrid2d(x1, x2, self.c2d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = herm.hermgrid2d(z, z, self.c2d)
+        assert_(res.shape == (2, 3)*2)
+
+    def test_hermgrid3d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test values
+        tgt = np.einsum('i,j,k->ijk', y1, y2, y3)
+        res = herm.hermgrid3d(x1, x2, x3, self.c3d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = herm.hermgrid3d(z, z, z, self.c3d)
+        assert_(res.shape == (2, 3)*3)
+
+
+class TestIntegral:
+
+    def test_hermint(self):
+        # check exceptions
+        assert_raises(TypeError, herm.hermint, [0], .5)
+        assert_raises(ValueError, herm.hermint, [0], -1)
+        assert_raises(ValueError, herm.hermint, [0], 1, [0, 0])
+        assert_raises(ValueError, herm.hermint, [0], lbnd=[0])
+        assert_raises(ValueError, herm.hermint, [0], scl=[0])
+        assert_raises(TypeError, herm.hermint, [0], axis=.5)
+
+        # test integration of zero polynomial
+        for i in range(2, 5):
+            k = [0]*(i - 2) + [1]
+            res = herm.hermint([0], m=i, k=k)
+            assert_almost_equal(res, [0, .5])
+
+        # check single integration with integration constant
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            tgt = [i] + [0]*i + [1/scl]
+            hermpol = herm.poly2herm(pol)
+            hermint = herm.hermint(hermpol, m=1, k=[i])
+            res = herm.herm2poly(hermint)
+            assert_almost_equal(trim(res), trim(tgt))
+
+        # check single integration with integration constant and lbnd
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            hermpol = herm.poly2herm(pol)
+            hermint = herm.hermint(hermpol, m=1, k=[i], lbnd=-1)
+            assert_almost_equal(herm.hermval(-1, hermint), i)
+
+        # check single integration with integration constant and scaling
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            tgt = [i] + [0]*i + [2/scl]
+            hermpol = herm.poly2herm(pol)
+            hermint = herm.hermint(hermpol, m=1, k=[i], scl=2)
+            res = herm.herm2poly(hermint)
+            assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with default k
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = herm.hermint(tgt, m=1)
+                res = herm.hermint(pol, m=j)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with defined k
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = herm.hermint(tgt, m=1, k=[k])
+                res = herm.hermint(pol, m=j, k=list(range(j)))
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with lbnd
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = herm.hermint(tgt, m=1, k=[k], lbnd=-1)
+                res = herm.hermint(pol, m=j, k=list(range(j)), lbnd=-1)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with scaling
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = herm.hermint(tgt, m=1, k=[k], scl=2)
+                res = herm.hermint(pol, m=j, k=list(range(j)), scl=2)
+                assert_almost_equal(trim(res), trim(tgt))
+
+    def test_hermint_axis(self):
+        # check that axis keyword works
+        c2d = np.random.random((3, 4))
+
+        tgt = np.vstack([herm.hermint(c) for c in c2d.T]).T
+        res = herm.hermint(c2d, axis=0)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([herm.hermint(c) for c in c2d])
+        res = herm.hermint(c2d, axis=1)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([herm.hermint(c, k=3) for c in c2d])
+        res = herm.hermint(c2d, k=3, axis=1)
+        assert_almost_equal(res, tgt)
+
+
+class TestDerivative:
+
+    def test_hermder(self):
+        # check exceptions
+        assert_raises(TypeError, herm.hermder, [0], .5)
+        assert_raises(ValueError, herm.hermder, [0], -1)
+
+        # check that zeroth derivative does nothing
+        for i in range(5):
+            tgt = [0]*i + [1]
+            res = herm.hermder(tgt, m=0)
+            assert_equal(trim(res), trim(tgt))
+
+        # check that derivation is the inverse of integration
+        for i in range(5):
+            for j in range(2, 5):
+                tgt = [0]*i + [1]
+                res = herm.hermder(herm.hermint(tgt, m=j), m=j)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check derivation with scaling
+        for i in range(5):
+            for j in range(2, 5):
+                tgt = [0]*i + [1]
+                res = herm.hermder(herm.hermint(tgt, m=j, scl=2), m=j, scl=.5)
+                assert_almost_equal(trim(res), trim(tgt))
+
+    def test_hermder_axis(self):
+        # check that axis keyword works
+        c2d = np.random.random((3, 4))
+
+        tgt = np.vstack([herm.hermder(c) for c in c2d.T]).T
+        res = herm.hermder(c2d, axis=0)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([herm.hermder(c) for c in c2d])
+        res = herm.hermder(c2d, axis=1)
+        assert_almost_equal(res, tgt)
+
+
+class TestVander:
+    # some random values in [-1, 1)
+    x = np.random.random((3, 5))*2 - 1
+
+    def test_hermvander(self):
+        # check for 1d x
+        x = np.arange(3)
+        v = herm.hermvander(x, 3)
+        assert_(v.shape == (3, 4))
+        for i in range(4):
+            coef = [0]*i + [1]
+            assert_almost_equal(v[..., i], herm.hermval(x, coef))
+
+        # check for 2d x
+        x = np.array([[1, 2], [3, 4], [5, 6]])
+        v = herm.hermvander(x, 3)
+        assert_(v.shape == (3, 2, 4))
+        for i in range(4):
+            coef = [0]*i + [1]
+            assert_almost_equal(v[..., i], herm.hermval(x, coef))
+
+    def test_hermvander2d(self):
+        # also tests hermval2d for non-square coefficient array
+        x1, x2, x3 = self.x
+        c = np.random.random((2, 3))
+        van = herm.hermvander2d(x1, x2, [1, 2])
+        tgt = herm.hermval2d(x1, x2, c)
+        res = np.dot(van, c.flat)
+        assert_almost_equal(res, tgt)
+
+        # check shape
+        van = herm.hermvander2d([x1], [x2], [1, 2])
+        assert_(van.shape == (1, 5, 6))
+
+    def test_hermvander3d(self):
+        # also tests hermval3d for non-square coefficient array
+        x1, x2, x3 = self.x
+        c = np.random.random((2, 3, 4))
+        van = herm.hermvander3d(x1, x2, x3, [1, 2, 3])
+        tgt = herm.hermval3d(x1, x2, x3, c)
+        res = np.dot(van, c.flat)
+        assert_almost_equal(res, tgt)
+
+        # check shape
+        van = herm.hermvander3d([x1], [x2], [x3], [1, 2, 3])
+        assert_(van.shape == (1, 5, 24))
+
+
+class TestFitting:
+
+    def test_hermfit(self):
+        def f(x):
+            return x*(x - 1)*(x - 2)
+
+        def f2(x):
+            return x**4 + x**2 + 1
+
+        # Test exceptions
+        assert_raises(ValueError, herm.hermfit, [1], [1], -1)
+        assert_raises(TypeError, herm.hermfit, [[1]], [1], 0)
+        assert_raises(TypeError, herm.hermfit, [], [1], 0)
+        assert_raises(TypeError, herm.hermfit, [1], [[[1]]], 0)
+        assert_raises(TypeError, herm.hermfit, [1, 2], [1], 0)
+        assert_raises(TypeError, herm.hermfit, [1], [1, 2], 0)
+        assert_raises(TypeError, herm.hermfit, [1], [1], 0, w=[[1]])
+        assert_raises(TypeError, herm.hermfit, [1], [1], 0, w=[1, 1])
+        assert_raises(ValueError, herm.hermfit, [1], [1], [-1,])
+        assert_raises(ValueError, herm.hermfit, [1], [1], [2, -1, 6])
+        assert_raises(TypeError, herm.hermfit, [1], [1], [])
+
+        # Test fit
+        x = np.linspace(0, 2)
+        y = f(x)
+        #
+        coef3 = herm.hermfit(x, y, 3)
+        assert_equal(len(coef3), 4)
+        assert_almost_equal(herm.hermval(x, coef3), y)
+        coef3 = herm.hermfit(x, y, [0, 1, 2, 3])
+        assert_equal(len(coef3), 4)
+        assert_almost_equal(herm.hermval(x, coef3), y)
+        #
+        coef4 = herm.hermfit(x, y, 4)
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(herm.hermval(x, coef4), y)
+        coef4 = herm.hermfit(x, y, [0, 1, 2, 3, 4])
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(herm.hermval(x, coef4), y)
+        # check things still work if deg is not in strict increasing
+        coef4 = herm.hermfit(x, y, [2, 3, 4, 1, 0])
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(herm.hermval(x, coef4), y)
+        #
+        coef2d = herm.hermfit(x, np.array([y, y]).T, 3)
+        assert_almost_equal(coef2d, np.array([coef3, coef3]).T)
+        coef2d = herm.hermfit(x, np.array([y, y]).T, [0, 1, 2, 3])
+        assert_almost_equal(coef2d, np.array([coef3, coef3]).T)
+        # test weighting
+        w = np.zeros_like(x)
+        yw = y.copy()
+        w[1::2] = 1
+        y[0::2] = 0
+        wcoef3 = herm.hermfit(x, yw, 3, w=w)
+        assert_almost_equal(wcoef3, coef3)
+        wcoef3 = herm.hermfit(x, yw, [0, 1, 2, 3], w=w)
+        assert_almost_equal(wcoef3, coef3)
+        #
+        wcoef2d = herm.hermfit(x, np.array([yw, yw]).T, 3, w=w)
+        assert_almost_equal(wcoef2d, np.array([coef3, coef3]).T)
+        wcoef2d = herm.hermfit(x, np.array([yw, yw]).T, [0, 1, 2, 3], w=w)
+        assert_almost_equal(wcoef2d, np.array([coef3, coef3]).T)
+        # test scaling with complex values x points whose square
+        # is zero when summed.
+        x = [1, 1j, -1, -1j]
+        assert_almost_equal(herm.hermfit(x, x, 1), [0, .5])
+        assert_almost_equal(herm.hermfit(x, x, [0, 1]), [0, .5])
+        # test fitting only even Legendre polynomials
+        x = np.linspace(-1, 1)
+        y = f2(x)
+        coef1 = herm.hermfit(x, y, 4)
+        assert_almost_equal(herm.hermval(x, coef1), y)
+        coef2 = herm.hermfit(x, y, [0, 2, 4])
+        assert_almost_equal(herm.hermval(x, coef2), y)
+        assert_almost_equal(coef1, coef2)
+
+
+class TestCompanion:
+
+    def test_raises(self):
+        assert_raises(ValueError, herm.hermcompanion, [])
+        assert_raises(ValueError, herm.hermcompanion, [1])
+
+    def test_dimensions(self):
+        for i in range(1, 5):
+            coef = [0]*i + [1]
+            assert_(herm.hermcompanion(coef).shape == (i, i))
+
+    def test_linear_root(self):
+        assert_(herm.hermcompanion([1, 2])[0, 0] == -.25)
+
+
+class TestGauss:
+
+    def test_100(self):
+        x, w = herm.hermgauss(100)
+
+        # test orthogonality. Note that the results need to be normalized,
+        # otherwise the huge values that can arise from fast growing
+        # functions like Laguerre can be very confusing.
+        v = herm.hermvander(x, 99)
+        vv = np.dot(v.T * w, v)
+        vd = 1/np.sqrt(vv.diagonal())
+        vv = vd[:, None] * vv * vd
+        assert_almost_equal(vv, np.eye(100))
+
+        # check that the integral of 1 is correct
+        tgt = np.sqrt(np.pi)
+        assert_almost_equal(w.sum(), tgt)
+
+
+class TestMisc:
+
+    def test_hermfromroots(self):
+        res = herm.hermfromroots([])
+        assert_almost_equal(trim(res), [1])
+        for i in range(1, 5):
+            roots = np.cos(np.linspace(-np.pi, 0, 2*i + 1)[1::2])
+            pol = herm.hermfromroots(roots)
+            res = herm.hermval(roots, pol)
+            tgt = 0
+            assert_(len(pol) == i + 1)
+            assert_almost_equal(herm.herm2poly(pol)[-1], 1)
+            assert_almost_equal(res, tgt)
+
+    def test_hermroots(self):
+        assert_almost_equal(herm.hermroots([1]), [])
+        assert_almost_equal(herm.hermroots([1, 1]), [-.5])
+        for i in range(2, 5):
+            tgt = np.linspace(-1, 1, i)
+            res = herm.hermroots(herm.hermfromroots(tgt))
+            assert_almost_equal(trim(res), trim(tgt))
+
+    def test_hermtrim(self):
+        coef = [2, -1, 1, 0]
+
+        # Test exceptions
+        assert_raises(ValueError, herm.hermtrim, coef, -1)
+
+        # Test results
+        assert_equal(herm.hermtrim(coef), coef[:-1])
+        assert_equal(herm.hermtrim(coef, 1), coef[:-3])
+        assert_equal(herm.hermtrim(coef, 2), [0])
+
+    def test_hermline(self):
+        assert_equal(herm.hermline(3, 4), [3, 2])
+
+    def test_herm2poly(self):
+        for i in range(10):
+            assert_almost_equal(herm.herm2poly([0]*i + [1]), Hlist[i])
+
+    def test_poly2herm(self):
+        for i in range(10):
+            assert_almost_equal(herm.poly2herm(Hlist[i]), [0]*i + [1])
+
+    def test_weight(self):
+        x = np.linspace(-5, 5, 11)
+        tgt = np.exp(-x**2)
+        res = herm.hermweight(x)
+        assert_almost_equal(res, tgt)
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_hermite_e.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_hermite_e.py
new file mode 100644
index 00000000..2d262a33
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_hermite_e.py
@@ -0,0 +1,556 @@
+"""Tests for hermite_e module.
+
+"""
+from functools import reduce
+
+import numpy as np
+import numpy.polynomial.hermite_e as herme
+from numpy.polynomial.polynomial import polyval
+from numpy.testing import (
+    assert_almost_equal, assert_raises, assert_equal, assert_,
+    )
+
+He0 = np.array([1])
+He1 = np.array([0, 1])
+He2 = np.array([-1, 0, 1])
+He3 = np.array([0, -3, 0, 1])
+He4 = np.array([3, 0, -6, 0, 1])
+He5 = np.array([0, 15, 0, -10, 0, 1])
+He6 = np.array([-15, 0, 45, 0, -15, 0, 1])
+He7 = np.array([0, -105, 0, 105, 0, -21, 0, 1])
+He8 = np.array([105, 0, -420, 0, 210, 0, -28, 0, 1])
+He9 = np.array([0, 945, 0, -1260, 0, 378, 0, -36, 0, 1])
+
+Helist = [He0, He1, He2, He3, He4, He5, He6, He7, He8, He9]
+
+
+def trim(x):
+    return herme.hermetrim(x, tol=1e-6)
+
+
+class TestConstants:
+
+    def test_hermedomain(self):
+        assert_equal(herme.hermedomain, [-1, 1])
+
+    def test_hermezero(self):
+        assert_equal(herme.hermezero, [0])
+
+    def test_hermeone(self):
+        assert_equal(herme.hermeone, [1])
+
+    def test_hermex(self):
+        assert_equal(herme.hermex, [0, 1])
+
+
+class TestArithmetic:
+    x = np.linspace(-3, 3, 100)
+
+    def test_hermeadd(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(max(i, j) + 1)
+                tgt[i] += 1
+                tgt[j] += 1
+                res = herme.hermeadd([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_hermesub(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(max(i, j) + 1)
+                tgt[i] += 1
+                tgt[j] -= 1
+                res = herme.hermesub([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_hermemulx(self):
+        assert_equal(herme.hermemulx([0]), [0])
+        assert_equal(herme.hermemulx([1]), [0, 1])
+        for i in range(1, 5):
+            ser = [0]*i + [1]
+            tgt = [0]*(i - 1) + [i, 0, 1]
+            assert_equal(herme.hermemulx(ser), tgt)
+
+    def test_hermemul(self):
+        # check values of result
+        for i in range(5):
+            pol1 = [0]*i + [1]
+            val1 = herme.hermeval(self.x, pol1)
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                pol2 = [0]*j + [1]
+                val2 = herme.hermeval(self.x, pol2)
+                pol3 = herme.hermemul(pol1, pol2)
+                val3 = herme.hermeval(self.x, pol3)
+                assert_(len(pol3) == i + j + 1, msg)
+                assert_almost_equal(val3, val1*val2, err_msg=msg)
+
+    def test_hermediv(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                ci = [0]*i + [1]
+                cj = [0]*j + [1]
+                tgt = herme.hermeadd(ci, cj)
+                quo, rem = herme.hermediv(tgt, ci)
+                res = herme.hermeadd(herme.hermemul(quo, ci), rem)
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_hermepow(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                c = np.arange(i + 1)
+                tgt = reduce(herme.hermemul, [c]*j, np.array([1]))
+                res = herme.hermepow(c, j)
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+
+class TestEvaluation:
+    # coefficients of 1 + 2*x + 3*x**2
+    c1d = np.array([4., 2., 3.])
+    c2d = np.einsum('i,j->ij', c1d, c1d)
+    c3d = np.einsum('i,j,k->ijk', c1d, c1d, c1d)
+
+    # some random values in [-1, 1)
+    x = np.random.random((3, 5))*2 - 1
+    y = polyval(x, [1., 2., 3.])
+
+    def test_hermeval(self):
+        #check empty input
+        assert_equal(herme.hermeval([], [1]).size, 0)
+
+        #check normal input)
+        x = np.linspace(-1, 1)
+        y = [polyval(x, c) for c in Helist]
+        for i in range(10):
+            msg = f"At i={i}"
+            tgt = y[i]
+            res = herme.hermeval(x, [0]*i + [1])
+            assert_almost_equal(res, tgt, err_msg=msg)
+
+        #check that shape is preserved
+        for i in range(3):
+            dims = [2]*i
+            x = np.zeros(dims)
+            assert_equal(herme.hermeval(x, [1]).shape, dims)
+            assert_equal(herme.hermeval(x, [1, 0]).shape, dims)
+            assert_equal(herme.hermeval(x, [1, 0, 0]).shape, dims)
+
+    def test_hermeval2d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test exceptions
+        assert_raises(ValueError, herme.hermeval2d, x1, x2[:2], self.c2d)
+
+        #test values
+        tgt = y1*y2
+        res = herme.hermeval2d(x1, x2, self.c2d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = herme.hermeval2d(z, z, self.c2d)
+        assert_(res.shape == (2, 3))
+
+    def test_hermeval3d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test exceptions
+        assert_raises(ValueError, herme.hermeval3d, x1, x2, x3[:2], self.c3d)
+
+        #test values
+        tgt = y1*y2*y3
+        res = herme.hermeval3d(x1, x2, x3, self.c3d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = herme.hermeval3d(z, z, z, self.c3d)
+        assert_(res.shape == (2, 3))
+
+    def test_hermegrid2d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test values
+        tgt = np.einsum('i,j->ij', y1, y2)
+        res = herme.hermegrid2d(x1, x2, self.c2d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = herme.hermegrid2d(z, z, self.c2d)
+        assert_(res.shape == (2, 3)*2)
+
+    def test_hermegrid3d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test values
+        tgt = np.einsum('i,j,k->ijk', y1, y2, y3)
+        res = herme.hermegrid3d(x1, x2, x3, self.c3d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = herme.hermegrid3d(z, z, z, self.c3d)
+        assert_(res.shape == (2, 3)*3)
+
+
+class TestIntegral:
+
+    def test_hermeint(self):
+        # check exceptions
+        assert_raises(TypeError, herme.hermeint, [0], .5)
+        assert_raises(ValueError, herme.hermeint, [0], -1)
+        assert_raises(ValueError, herme.hermeint, [0], 1, [0, 0])
+        assert_raises(ValueError, herme.hermeint, [0], lbnd=[0])
+        assert_raises(ValueError, herme.hermeint, [0], scl=[0])
+        assert_raises(TypeError, herme.hermeint, [0], axis=.5)
+
+        # test integration of zero polynomial
+        for i in range(2, 5):
+            k = [0]*(i - 2) + [1]
+            res = herme.hermeint([0], m=i, k=k)
+            assert_almost_equal(res, [0, 1])
+
+        # check single integration with integration constant
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            tgt = [i] + [0]*i + [1/scl]
+            hermepol = herme.poly2herme(pol)
+            hermeint = herme.hermeint(hermepol, m=1, k=[i])
+            res = herme.herme2poly(hermeint)
+            assert_almost_equal(trim(res), trim(tgt))
+
+        # check single integration with integration constant and lbnd
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            hermepol = herme.poly2herme(pol)
+            hermeint = herme.hermeint(hermepol, m=1, k=[i], lbnd=-1)
+            assert_almost_equal(herme.hermeval(-1, hermeint), i)
+
+        # check single integration with integration constant and scaling
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            tgt = [i] + [0]*i + [2/scl]
+            hermepol = herme.poly2herme(pol)
+            hermeint = herme.hermeint(hermepol, m=1, k=[i], scl=2)
+            res = herme.herme2poly(hermeint)
+            assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with default k
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = herme.hermeint(tgt, m=1)
+                res = herme.hermeint(pol, m=j)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with defined k
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = herme.hermeint(tgt, m=1, k=[k])
+                res = herme.hermeint(pol, m=j, k=list(range(j)))
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with lbnd
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = herme.hermeint(tgt, m=1, k=[k], lbnd=-1)
+                res = herme.hermeint(pol, m=j, k=list(range(j)), lbnd=-1)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with scaling
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = herme.hermeint(tgt, m=1, k=[k], scl=2)
+                res = herme.hermeint(pol, m=j, k=list(range(j)), scl=2)
+                assert_almost_equal(trim(res), trim(tgt))
+
+    def test_hermeint_axis(self):
+        # check that axis keyword works
+        c2d = np.random.random((3, 4))
+
+        tgt = np.vstack([herme.hermeint(c) for c in c2d.T]).T
+        res = herme.hermeint(c2d, axis=0)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([herme.hermeint(c) for c in c2d])
+        res = herme.hermeint(c2d, axis=1)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([herme.hermeint(c, k=3) for c in c2d])
+        res = herme.hermeint(c2d, k=3, axis=1)
+        assert_almost_equal(res, tgt)
+
+
+class TestDerivative:
+
+    def test_hermeder(self):
+        # check exceptions
+        assert_raises(TypeError, herme.hermeder, [0], .5)
+        assert_raises(ValueError, herme.hermeder, [0], -1)
+
+        # check that zeroth derivative does nothing
+        for i in range(5):
+            tgt = [0]*i + [1]
+            res = herme.hermeder(tgt, m=0)
+            assert_equal(trim(res), trim(tgt))
+
+        # check that derivation is the inverse of integration
+        for i in range(5):
+            for j in range(2, 5):
+                tgt = [0]*i + [1]
+                res = herme.hermeder(herme.hermeint(tgt, m=j), m=j)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check derivation with scaling
+        for i in range(5):
+            for j in range(2, 5):
+                tgt = [0]*i + [1]
+                res = herme.hermeder(
+                    herme.hermeint(tgt, m=j, scl=2), m=j, scl=.5)
+                assert_almost_equal(trim(res), trim(tgt))
+
+    def test_hermeder_axis(self):
+        # check that axis keyword works
+        c2d = np.random.random((3, 4))
+
+        tgt = np.vstack([herme.hermeder(c) for c in c2d.T]).T
+        res = herme.hermeder(c2d, axis=0)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([herme.hermeder(c) for c in c2d])
+        res = herme.hermeder(c2d, axis=1)
+        assert_almost_equal(res, tgt)
+
+
+class TestVander:
+    # some random values in [-1, 1)
+    x = np.random.random((3, 5))*2 - 1
+
+    def test_hermevander(self):
+        # check for 1d x
+        x = np.arange(3)
+        v = herme.hermevander(x, 3)
+        assert_(v.shape == (3, 4))
+        for i in range(4):
+            coef = [0]*i + [1]
+            assert_almost_equal(v[..., i], herme.hermeval(x, coef))
+
+        # check for 2d x
+        x = np.array([[1, 2], [3, 4], [5, 6]])
+        v = herme.hermevander(x, 3)
+        assert_(v.shape == (3, 2, 4))
+        for i in range(4):
+            coef = [0]*i + [1]
+            assert_almost_equal(v[..., i], herme.hermeval(x, coef))
+
+    def test_hermevander2d(self):
+        # also tests hermeval2d for non-square coefficient array
+        x1, x2, x3 = self.x
+        c = np.random.random((2, 3))
+        van = herme.hermevander2d(x1, x2, [1, 2])
+        tgt = herme.hermeval2d(x1, x2, c)
+        res = np.dot(van, c.flat)
+        assert_almost_equal(res, tgt)
+
+        # check shape
+        van = herme.hermevander2d([x1], [x2], [1, 2])
+        assert_(van.shape == (1, 5, 6))
+
+    def test_hermevander3d(self):
+        # also tests hermeval3d for non-square coefficient array
+        x1, x2, x3 = self.x
+        c = np.random.random((2, 3, 4))
+        van = herme.hermevander3d(x1, x2, x3, [1, 2, 3])
+        tgt = herme.hermeval3d(x1, x2, x3, c)
+        res = np.dot(van, c.flat)
+        assert_almost_equal(res, tgt)
+
+        # check shape
+        van = herme.hermevander3d([x1], [x2], [x3], [1, 2, 3])
+        assert_(van.shape == (1, 5, 24))
+
+
+class TestFitting:
+
+    def test_hermefit(self):
+        def f(x):
+            return x*(x - 1)*(x - 2)
+
+        def f2(x):
+            return x**4 + x**2 + 1
+
+        # Test exceptions
+        assert_raises(ValueError, herme.hermefit, [1], [1], -1)
+        assert_raises(TypeError, herme.hermefit, [[1]], [1], 0)
+        assert_raises(TypeError, herme.hermefit, [], [1], 0)
+        assert_raises(TypeError, herme.hermefit, [1], [[[1]]], 0)
+        assert_raises(TypeError, herme.hermefit, [1, 2], [1], 0)
+        assert_raises(TypeError, herme.hermefit, [1], [1, 2], 0)
+        assert_raises(TypeError, herme.hermefit, [1], [1], 0, w=[[1]])
+        assert_raises(TypeError, herme.hermefit, [1], [1], 0, w=[1, 1])
+        assert_raises(ValueError, herme.hermefit, [1], [1], [-1,])
+        assert_raises(ValueError, herme.hermefit, [1], [1], [2, -1, 6])
+        assert_raises(TypeError, herme.hermefit, [1], [1], [])
+
+        # Test fit
+        x = np.linspace(0, 2)
+        y = f(x)
+        #
+        coef3 = herme.hermefit(x, y, 3)
+        assert_equal(len(coef3), 4)
+        assert_almost_equal(herme.hermeval(x, coef3), y)
+        coef3 = herme.hermefit(x, y, [0, 1, 2, 3])
+        assert_equal(len(coef3), 4)
+        assert_almost_equal(herme.hermeval(x, coef3), y)
+        #
+        coef4 = herme.hermefit(x, y, 4)
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(herme.hermeval(x, coef4), y)
+        coef4 = herme.hermefit(x, y, [0, 1, 2, 3, 4])
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(herme.hermeval(x, coef4), y)
+        # check things still work if deg is not in strict increasing
+        coef4 = herme.hermefit(x, y, [2, 3, 4, 1, 0])
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(herme.hermeval(x, coef4), y)
+        #
+        coef2d = herme.hermefit(x, np.array([y, y]).T, 3)
+        assert_almost_equal(coef2d, np.array([coef3, coef3]).T)
+        coef2d = herme.hermefit(x, np.array([y, y]).T, [0, 1, 2, 3])
+        assert_almost_equal(coef2d, np.array([coef3, coef3]).T)
+        # test weighting
+        w = np.zeros_like(x)
+        yw = y.copy()
+        w[1::2] = 1
+        y[0::2] = 0
+        wcoef3 = herme.hermefit(x, yw, 3, w=w)
+        assert_almost_equal(wcoef3, coef3)
+        wcoef3 = herme.hermefit(x, yw, [0, 1, 2, 3], w=w)
+        assert_almost_equal(wcoef3, coef3)
+        #
+        wcoef2d = herme.hermefit(x, np.array([yw, yw]).T, 3, w=w)
+        assert_almost_equal(wcoef2d, np.array([coef3, coef3]).T)
+        wcoef2d = herme.hermefit(x, np.array([yw, yw]).T, [0, 1, 2, 3], w=w)
+        assert_almost_equal(wcoef2d, np.array([coef3, coef3]).T)
+        # test scaling with complex values x points whose square
+        # is zero when summed.
+        x = [1, 1j, -1, -1j]
+        assert_almost_equal(herme.hermefit(x, x, 1), [0, 1])
+        assert_almost_equal(herme.hermefit(x, x, [0, 1]), [0, 1])
+        # test fitting only even Legendre polynomials
+        x = np.linspace(-1, 1)
+        y = f2(x)
+        coef1 = herme.hermefit(x, y, 4)
+        assert_almost_equal(herme.hermeval(x, coef1), y)
+        coef2 = herme.hermefit(x, y, [0, 2, 4])
+        assert_almost_equal(herme.hermeval(x, coef2), y)
+        assert_almost_equal(coef1, coef2)
+
+
+class TestCompanion:
+
+    def test_raises(self):
+        assert_raises(ValueError, herme.hermecompanion, [])
+        assert_raises(ValueError, herme.hermecompanion, [1])
+
+    def test_dimensions(self):
+        for i in range(1, 5):
+            coef = [0]*i + [1]
+            assert_(herme.hermecompanion(coef).shape == (i, i))
+
+    def test_linear_root(self):
+        assert_(herme.hermecompanion([1, 2])[0, 0] == -.5)
+
+
+class TestGauss:
+
+    def test_100(self):
+        x, w = herme.hermegauss(100)
+
+        # test orthogonality. Note that the results need to be normalized,
+        # otherwise the huge values that can arise from fast growing
+        # functions like Laguerre can be very confusing.
+        v = herme.hermevander(x, 99)
+        vv = np.dot(v.T * w, v)
+        vd = 1/np.sqrt(vv.diagonal())
+        vv = vd[:, None] * vv * vd
+        assert_almost_equal(vv, np.eye(100))
+
+        # check that the integral of 1 is correct
+        tgt = np.sqrt(2*np.pi)
+        assert_almost_equal(w.sum(), tgt)
+
+
+class TestMisc:
+
+    def test_hermefromroots(self):
+        res = herme.hermefromroots([])
+        assert_almost_equal(trim(res), [1])
+        for i in range(1, 5):
+            roots = np.cos(np.linspace(-np.pi, 0, 2*i + 1)[1::2])
+            pol = herme.hermefromroots(roots)
+            res = herme.hermeval(roots, pol)
+            tgt = 0
+            assert_(len(pol) == i + 1)
+            assert_almost_equal(herme.herme2poly(pol)[-1], 1)
+            assert_almost_equal(res, tgt)
+
+    def test_hermeroots(self):
+        assert_almost_equal(herme.hermeroots([1]), [])
+        assert_almost_equal(herme.hermeroots([1, 1]), [-1])
+        for i in range(2, 5):
+            tgt = np.linspace(-1, 1, i)
+            res = herme.hermeroots(herme.hermefromroots(tgt))
+            assert_almost_equal(trim(res), trim(tgt))
+
+    def test_hermetrim(self):
+        coef = [2, -1, 1, 0]
+
+        # Test exceptions
+        assert_raises(ValueError, herme.hermetrim, coef, -1)
+
+        # Test results
+        assert_equal(herme.hermetrim(coef), coef[:-1])
+        assert_equal(herme.hermetrim(coef, 1), coef[:-3])
+        assert_equal(herme.hermetrim(coef, 2), [0])
+
+    def test_hermeline(self):
+        assert_equal(herme.hermeline(3, 4), [3, 4])
+
+    def test_herme2poly(self):
+        for i in range(10):
+            assert_almost_equal(herme.herme2poly([0]*i + [1]), Helist[i])
+
+    def test_poly2herme(self):
+        for i in range(10):
+            assert_almost_equal(herme.poly2herme(Helist[i]), [0]*i + [1])
+
+    def test_weight(self):
+        x = np.linspace(-5, 5, 11)
+        tgt = np.exp(-.5*x**2)
+        res = herme.hermeweight(x)
+        assert_almost_equal(res, tgt)
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_laguerre.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_laguerre.py
new file mode 100644
index 00000000..227ef3c5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_laguerre.py
@@ -0,0 +1,537 @@
+"""Tests for laguerre module.
+
+"""
+from functools import reduce
+
+import numpy as np
+import numpy.polynomial.laguerre as lag
+from numpy.polynomial.polynomial import polyval
+from numpy.testing import (
+    assert_almost_equal, assert_raises, assert_equal, assert_,
+    )
+
+L0 = np.array([1])/1
+L1 = np.array([1, -1])/1
+L2 = np.array([2, -4, 1])/2
+L3 = np.array([6, -18, 9, -1])/6
+L4 = np.array([24, -96, 72, -16, 1])/24
+L5 = np.array([120, -600, 600, -200, 25, -1])/120
+L6 = np.array([720, -4320, 5400, -2400, 450, -36, 1])/720
+
+Llist = [L0, L1, L2, L3, L4, L5, L6]
+
+
+def trim(x):
+    return lag.lagtrim(x, tol=1e-6)
+
+
+class TestConstants:
+
+    def test_lagdomain(self):
+        assert_equal(lag.lagdomain, [0, 1])
+
+    def test_lagzero(self):
+        assert_equal(lag.lagzero, [0])
+
+    def test_lagone(self):
+        assert_equal(lag.lagone, [1])
+
+    def test_lagx(self):
+        assert_equal(lag.lagx, [1, -1])
+
+
+class TestArithmetic:
+    x = np.linspace(-3, 3, 100)
+
+    def test_lagadd(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(max(i, j) + 1)
+                tgt[i] += 1
+                tgt[j] += 1
+                res = lag.lagadd([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_lagsub(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(max(i, j) + 1)
+                tgt[i] += 1
+                tgt[j] -= 1
+                res = lag.lagsub([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_lagmulx(self):
+        assert_equal(lag.lagmulx([0]), [0])
+        assert_equal(lag.lagmulx([1]), [1, -1])
+        for i in range(1, 5):
+            ser = [0]*i + [1]
+            tgt = [0]*(i - 1) + [-i, 2*i + 1, -(i + 1)]
+            assert_almost_equal(lag.lagmulx(ser), tgt)
+
+    def test_lagmul(self):
+        # check values of result
+        for i in range(5):
+            pol1 = [0]*i + [1]
+            val1 = lag.lagval(self.x, pol1)
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                pol2 = [0]*j + [1]
+                val2 = lag.lagval(self.x, pol2)
+                pol3 = lag.lagmul(pol1, pol2)
+                val3 = lag.lagval(self.x, pol3)
+                assert_(len(pol3) == i + j + 1, msg)
+                assert_almost_equal(val3, val1*val2, err_msg=msg)
+
+    def test_lagdiv(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                ci = [0]*i + [1]
+                cj = [0]*j + [1]
+                tgt = lag.lagadd(ci, cj)
+                quo, rem = lag.lagdiv(tgt, ci)
+                res = lag.lagadd(lag.lagmul(quo, ci), rem)
+                assert_almost_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_lagpow(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                c = np.arange(i + 1)
+                tgt = reduce(lag.lagmul, [c]*j, np.array([1]))
+                res = lag.lagpow(c, j) 
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+
+class TestEvaluation:
+    # coefficients of 1 + 2*x + 3*x**2
+    c1d = np.array([9., -14., 6.])
+    c2d = np.einsum('i,j->ij', c1d, c1d)
+    c3d = np.einsum('i,j,k->ijk', c1d, c1d, c1d)
+
+    # some random values in [-1, 1)
+    x = np.random.random((3, 5))*2 - 1
+    y = polyval(x, [1., 2., 3.])
+
+    def test_lagval(self):
+        #check empty input
+        assert_equal(lag.lagval([], [1]).size, 0)
+
+        #check normal input)
+        x = np.linspace(-1, 1)
+        y = [polyval(x, c) for c in Llist]
+        for i in range(7):
+            msg = f"At i={i}"
+            tgt = y[i]
+            res = lag.lagval(x, [0]*i + [1])
+            assert_almost_equal(res, tgt, err_msg=msg)
+
+        #check that shape is preserved
+        for i in range(3):
+            dims = [2]*i
+            x = np.zeros(dims)
+            assert_equal(lag.lagval(x, [1]).shape, dims)
+            assert_equal(lag.lagval(x, [1, 0]).shape, dims)
+            assert_equal(lag.lagval(x, [1, 0, 0]).shape, dims)
+
+    def test_lagval2d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test exceptions
+        assert_raises(ValueError, lag.lagval2d, x1, x2[:2], self.c2d)
+
+        #test values
+        tgt = y1*y2
+        res = lag.lagval2d(x1, x2, self.c2d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = lag.lagval2d(z, z, self.c2d)
+        assert_(res.shape == (2, 3))
+
+    def test_lagval3d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test exceptions
+        assert_raises(ValueError, lag.lagval3d, x1, x2, x3[:2], self.c3d)
+
+        #test values
+        tgt = y1*y2*y3
+        res = lag.lagval3d(x1, x2, x3, self.c3d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = lag.lagval3d(z, z, z, self.c3d)
+        assert_(res.shape == (2, 3))
+
+    def test_laggrid2d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test values
+        tgt = np.einsum('i,j->ij', y1, y2)
+        res = lag.laggrid2d(x1, x2, self.c2d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = lag.laggrid2d(z, z, self.c2d)
+        assert_(res.shape == (2, 3)*2)
+
+    def test_laggrid3d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test values
+        tgt = np.einsum('i,j,k->ijk', y1, y2, y3)
+        res = lag.laggrid3d(x1, x2, x3, self.c3d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = lag.laggrid3d(z, z, z, self.c3d)
+        assert_(res.shape == (2, 3)*3)
+
+
+class TestIntegral:
+
+    def test_lagint(self):
+        # check exceptions
+        assert_raises(TypeError, lag.lagint, [0], .5)
+        assert_raises(ValueError, lag.lagint, [0], -1)
+        assert_raises(ValueError, lag.lagint, [0], 1, [0, 0])
+        assert_raises(ValueError, lag.lagint, [0], lbnd=[0])
+        assert_raises(ValueError, lag.lagint, [0], scl=[0])
+        assert_raises(TypeError, lag.lagint, [0], axis=.5)
+
+        # test integration of zero polynomial
+        for i in range(2, 5):
+            k = [0]*(i - 2) + [1]
+            res = lag.lagint([0], m=i, k=k)
+            assert_almost_equal(res, [1, -1])
+
+        # check single integration with integration constant
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            tgt = [i] + [0]*i + [1/scl]
+            lagpol = lag.poly2lag(pol)
+            lagint = lag.lagint(lagpol, m=1, k=[i])
+            res = lag.lag2poly(lagint)
+            assert_almost_equal(trim(res), trim(tgt))
+
+        # check single integration with integration constant and lbnd
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            lagpol = lag.poly2lag(pol)
+            lagint = lag.lagint(lagpol, m=1, k=[i], lbnd=-1)
+            assert_almost_equal(lag.lagval(-1, lagint), i)
+
+        # check single integration with integration constant and scaling
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            tgt = [i] + [0]*i + [2/scl]
+            lagpol = lag.poly2lag(pol)
+            lagint = lag.lagint(lagpol, m=1, k=[i], scl=2)
+            res = lag.lag2poly(lagint)
+            assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with default k
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = lag.lagint(tgt, m=1)
+                res = lag.lagint(pol, m=j)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with defined k
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = lag.lagint(tgt, m=1, k=[k])
+                res = lag.lagint(pol, m=j, k=list(range(j)))
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with lbnd
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = lag.lagint(tgt, m=1, k=[k], lbnd=-1)
+                res = lag.lagint(pol, m=j, k=list(range(j)), lbnd=-1)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with scaling
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = lag.lagint(tgt, m=1, k=[k], scl=2)
+                res = lag.lagint(pol, m=j, k=list(range(j)), scl=2)
+                assert_almost_equal(trim(res), trim(tgt))
+
+    def test_lagint_axis(self):
+        # check that axis keyword works
+        c2d = np.random.random((3, 4))
+
+        tgt = np.vstack([lag.lagint(c) for c in c2d.T]).T
+        res = lag.lagint(c2d, axis=0)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([lag.lagint(c) for c in c2d])
+        res = lag.lagint(c2d, axis=1)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([lag.lagint(c, k=3) for c in c2d])
+        res = lag.lagint(c2d, k=3, axis=1)
+        assert_almost_equal(res, tgt)
+
+
+class TestDerivative:
+
+    def test_lagder(self):
+        # check exceptions
+        assert_raises(TypeError, lag.lagder, [0], .5)
+        assert_raises(ValueError, lag.lagder, [0], -1)
+
+        # check that zeroth derivative does nothing
+        for i in range(5):
+            tgt = [0]*i + [1]
+            res = lag.lagder(tgt, m=0)
+            assert_equal(trim(res), trim(tgt))
+
+        # check that derivation is the inverse of integration
+        for i in range(5):
+            for j in range(2, 5):
+                tgt = [0]*i + [1]
+                res = lag.lagder(lag.lagint(tgt, m=j), m=j)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check derivation with scaling
+        for i in range(5):
+            for j in range(2, 5):
+                tgt = [0]*i + [1]
+                res = lag.lagder(lag.lagint(tgt, m=j, scl=2), m=j, scl=.5)
+                assert_almost_equal(trim(res), trim(tgt))
+
+    def test_lagder_axis(self):
+        # check that axis keyword works
+        c2d = np.random.random((3, 4))
+
+        tgt = np.vstack([lag.lagder(c) for c in c2d.T]).T
+        res = lag.lagder(c2d, axis=0)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([lag.lagder(c) for c in c2d])
+        res = lag.lagder(c2d, axis=1)
+        assert_almost_equal(res, tgt)
+
+
+class TestVander:
+    # some random values in [-1, 1)
+    x = np.random.random((3, 5))*2 - 1
+
+    def test_lagvander(self):
+        # check for 1d x
+        x = np.arange(3)
+        v = lag.lagvander(x, 3)
+        assert_(v.shape == (3, 4))
+        for i in range(4):
+            coef = [0]*i + [1]
+            assert_almost_equal(v[..., i], lag.lagval(x, coef))
+
+        # check for 2d x
+        x = np.array([[1, 2], [3, 4], [5, 6]])
+        v = lag.lagvander(x, 3)
+        assert_(v.shape == (3, 2, 4))
+        for i in range(4):
+            coef = [0]*i + [1]
+            assert_almost_equal(v[..., i], lag.lagval(x, coef))
+
+    def test_lagvander2d(self):
+        # also tests lagval2d for non-square coefficient array
+        x1, x2, x3 = self.x
+        c = np.random.random((2, 3))
+        van = lag.lagvander2d(x1, x2, [1, 2])
+        tgt = lag.lagval2d(x1, x2, c)
+        res = np.dot(van, c.flat)
+        assert_almost_equal(res, tgt)
+
+        # check shape
+        van = lag.lagvander2d([x1], [x2], [1, 2])
+        assert_(van.shape == (1, 5, 6))
+
+    def test_lagvander3d(self):
+        # also tests lagval3d for non-square coefficient array
+        x1, x2, x3 = self.x
+        c = np.random.random((2, 3, 4))
+        van = lag.lagvander3d(x1, x2, x3, [1, 2, 3])
+        tgt = lag.lagval3d(x1, x2, x3, c)
+        res = np.dot(van, c.flat)
+        assert_almost_equal(res, tgt)
+
+        # check shape
+        van = lag.lagvander3d([x1], [x2], [x3], [1, 2, 3])
+        assert_(van.shape == (1, 5, 24))
+
+
+class TestFitting:
+
+    def test_lagfit(self):
+        def f(x):
+            return x*(x - 1)*(x - 2)
+
+        # Test exceptions
+        assert_raises(ValueError, lag.lagfit, [1], [1], -1)
+        assert_raises(TypeError, lag.lagfit, [[1]], [1], 0)
+        assert_raises(TypeError, lag.lagfit, [], [1], 0)
+        assert_raises(TypeError, lag.lagfit, [1], [[[1]]], 0)
+        assert_raises(TypeError, lag.lagfit, [1, 2], [1], 0)
+        assert_raises(TypeError, lag.lagfit, [1], [1, 2], 0)
+        assert_raises(TypeError, lag.lagfit, [1], [1], 0, w=[[1]])
+        assert_raises(TypeError, lag.lagfit, [1], [1], 0, w=[1, 1])
+        assert_raises(ValueError, lag.lagfit, [1], [1], [-1,])
+        assert_raises(ValueError, lag.lagfit, [1], [1], [2, -1, 6])
+        assert_raises(TypeError, lag.lagfit, [1], [1], [])
+
+        # Test fit
+        x = np.linspace(0, 2)
+        y = f(x)
+        #
+        coef3 = lag.lagfit(x, y, 3)
+        assert_equal(len(coef3), 4)
+        assert_almost_equal(lag.lagval(x, coef3), y)
+        coef3 = lag.lagfit(x, y, [0, 1, 2, 3])
+        assert_equal(len(coef3), 4)
+        assert_almost_equal(lag.lagval(x, coef3), y)
+        #
+        coef4 = lag.lagfit(x, y, 4)
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(lag.lagval(x, coef4), y)
+        coef4 = lag.lagfit(x, y, [0, 1, 2, 3, 4])
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(lag.lagval(x, coef4), y)
+        #
+        coef2d = lag.lagfit(x, np.array([y, y]).T, 3)
+        assert_almost_equal(coef2d, np.array([coef3, coef3]).T)
+        coef2d = lag.lagfit(x, np.array([y, y]).T, [0, 1, 2, 3])
+        assert_almost_equal(coef2d, np.array([coef3, coef3]).T)
+        # test weighting
+        w = np.zeros_like(x)
+        yw = y.copy()
+        w[1::2] = 1
+        y[0::2] = 0
+        wcoef3 = lag.lagfit(x, yw, 3, w=w)
+        assert_almost_equal(wcoef3, coef3)
+        wcoef3 = lag.lagfit(x, yw, [0, 1, 2, 3], w=w)
+        assert_almost_equal(wcoef3, coef3)
+        #
+        wcoef2d = lag.lagfit(x, np.array([yw, yw]).T, 3, w=w)
+        assert_almost_equal(wcoef2d, np.array([coef3, coef3]).T)
+        wcoef2d = lag.lagfit(x, np.array([yw, yw]).T, [0, 1, 2, 3], w=w)
+        assert_almost_equal(wcoef2d, np.array([coef3, coef3]).T)
+        # test scaling with complex values x points whose square
+        # is zero when summed.
+        x = [1, 1j, -1, -1j]
+        assert_almost_equal(lag.lagfit(x, x, 1), [1, -1])
+        assert_almost_equal(lag.lagfit(x, x, [0, 1]), [1, -1])
+
+
+class TestCompanion:
+
+    def test_raises(self):
+        assert_raises(ValueError, lag.lagcompanion, [])
+        assert_raises(ValueError, lag.lagcompanion, [1])
+
+    def test_dimensions(self):
+        for i in range(1, 5):
+            coef = [0]*i + [1]
+            assert_(lag.lagcompanion(coef).shape == (i, i))
+
+    def test_linear_root(self):
+        assert_(lag.lagcompanion([1, 2])[0, 0] == 1.5)
+
+
+class TestGauss:
+
+    def test_100(self):
+        x, w = lag.laggauss(100)
+
+        # test orthogonality. Note that the results need to be normalized,
+        # otherwise the huge values that can arise from fast growing
+        # functions like Laguerre can be very confusing.
+        v = lag.lagvander(x, 99)
+        vv = np.dot(v.T * w, v)
+        vd = 1/np.sqrt(vv.diagonal())
+        vv = vd[:, None] * vv * vd
+        assert_almost_equal(vv, np.eye(100))
+
+        # check that the integral of 1 is correct
+        tgt = 1.0
+        assert_almost_equal(w.sum(), tgt)
+
+
+class TestMisc:
+
+    def test_lagfromroots(self):
+        res = lag.lagfromroots([])
+        assert_almost_equal(trim(res), [1])
+        for i in range(1, 5):
+            roots = np.cos(np.linspace(-np.pi, 0, 2*i + 1)[1::2])
+            pol = lag.lagfromroots(roots)
+            res = lag.lagval(roots, pol)
+            tgt = 0
+            assert_(len(pol) == i + 1)
+            assert_almost_equal(lag.lag2poly(pol)[-1], 1)
+            assert_almost_equal(res, tgt)
+
+    def test_lagroots(self):
+        assert_almost_equal(lag.lagroots([1]), [])
+        assert_almost_equal(lag.lagroots([0, 1]), [1])
+        for i in range(2, 5):
+            tgt = np.linspace(0, 3, i)
+            res = lag.lagroots(lag.lagfromroots(tgt))
+            assert_almost_equal(trim(res), trim(tgt))
+
+    def test_lagtrim(self):
+        coef = [2, -1, 1, 0]
+
+        # Test exceptions
+        assert_raises(ValueError, lag.lagtrim, coef, -1)
+
+        # Test results
+        assert_equal(lag.lagtrim(coef), coef[:-1])
+        assert_equal(lag.lagtrim(coef, 1), coef[:-3])
+        assert_equal(lag.lagtrim(coef, 2), [0])
+
+    def test_lagline(self):
+        assert_equal(lag.lagline(3, 4), [7, -4])
+
+    def test_lag2poly(self):
+        for i in range(7):
+            assert_almost_equal(lag.lag2poly([0]*i + [1]), Llist[i])
+
+    def test_poly2lag(self):
+        for i in range(7):
+            assert_almost_equal(lag.poly2lag(Llist[i]), [0]*i + [1])
+
+    def test_weight(self):
+        x = np.linspace(0, 10, 11)
+        tgt = np.exp(-x)
+        res = lag.lagweight(x)
+        assert_almost_equal(res, tgt)
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_legendre.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_legendre.py
new file mode 100644
index 00000000..92399c16
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_legendre.py
@@ -0,0 +1,568 @@
+"""Tests for legendre module.
+
+"""
+from functools import reduce
+
+import numpy as np
+import numpy.polynomial.legendre as leg
+from numpy.polynomial.polynomial import polyval
+from numpy.testing import (
+    assert_almost_equal, assert_raises, assert_equal, assert_,
+    )
+
+L0 = np.array([1])
+L1 = np.array([0, 1])
+L2 = np.array([-1, 0, 3])/2
+L3 = np.array([0, -3, 0, 5])/2
+L4 = np.array([3, 0, -30, 0, 35])/8
+L5 = np.array([0, 15, 0, -70, 0, 63])/8
+L6 = np.array([-5, 0, 105, 0, -315, 0, 231])/16
+L7 = np.array([0, -35, 0, 315, 0, -693, 0, 429])/16
+L8 = np.array([35, 0, -1260, 0, 6930, 0, -12012, 0, 6435])/128
+L9 = np.array([0, 315, 0, -4620, 0, 18018, 0, -25740, 0, 12155])/128
+
+Llist = [L0, L1, L2, L3, L4, L5, L6, L7, L8, L9]
+
+
+def trim(x):
+    return leg.legtrim(x, tol=1e-6)
+
+
+class TestConstants:
+
+    def test_legdomain(self):
+        assert_equal(leg.legdomain, [-1, 1])
+
+    def test_legzero(self):
+        assert_equal(leg.legzero, [0])
+
+    def test_legone(self):
+        assert_equal(leg.legone, [1])
+
+    def test_legx(self):
+        assert_equal(leg.legx, [0, 1])
+
+
+class TestArithmetic:
+    x = np.linspace(-1, 1, 100)
+
+    def test_legadd(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(max(i, j) + 1)
+                tgt[i] += 1
+                tgt[j] += 1
+                res = leg.legadd([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_legsub(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(max(i, j) + 1)
+                tgt[i] += 1
+                tgt[j] -= 1
+                res = leg.legsub([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_legmulx(self):
+        assert_equal(leg.legmulx([0]), [0])
+        assert_equal(leg.legmulx([1]), [0, 1])
+        for i in range(1, 5):
+            tmp = 2*i + 1
+            ser = [0]*i + [1]
+            tgt = [0]*(i - 1) + [i/tmp, 0, (i + 1)/tmp]
+            assert_equal(leg.legmulx(ser), tgt)
+
+    def test_legmul(self):
+        # check values of result
+        for i in range(5):
+            pol1 = [0]*i + [1]
+            val1 = leg.legval(self.x, pol1)
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                pol2 = [0]*j + [1]
+                val2 = leg.legval(self.x, pol2)
+                pol3 = leg.legmul(pol1, pol2)
+                val3 = leg.legval(self.x, pol3)
+                assert_(len(pol3) == i + j + 1, msg)
+                assert_almost_equal(val3, val1*val2, err_msg=msg)
+
+    def test_legdiv(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                ci = [0]*i + [1]
+                cj = [0]*j + [1]
+                tgt = leg.legadd(ci, cj)
+                quo, rem = leg.legdiv(tgt, ci)
+                res = leg.legadd(leg.legmul(quo, ci), rem)
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_legpow(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                c = np.arange(i + 1)
+                tgt = reduce(leg.legmul, [c]*j, np.array([1]))
+                res = leg.legpow(c, j) 
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+
+class TestEvaluation:
+    # coefficients of 1 + 2*x + 3*x**2
+    c1d = np.array([2., 2., 2.])
+    c2d = np.einsum('i,j->ij', c1d, c1d)
+    c3d = np.einsum('i,j,k->ijk', c1d, c1d, c1d)
+
+    # some random values in [-1, 1)
+    x = np.random.random((3, 5))*2 - 1
+    y = polyval(x, [1., 2., 3.])
+
+    def test_legval(self):
+        #check empty input
+        assert_equal(leg.legval([], [1]).size, 0)
+
+        #check normal input)
+        x = np.linspace(-1, 1)
+        y = [polyval(x, c) for c in Llist]
+        for i in range(10):
+            msg = f"At i={i}"
+            tgt = y[i]
+            res = leg.legval(x, [0]*i + [1])
+            assert_almost_equal(res, tgt, err_msg=msg)
+
+        #check that shape is preserved
+        for i in range(3):
+            dims = [2]*i
+            x = np.zeros(dims)
+            assert_equal(leg.legval(x, [1]).shape, dims)
+            assert_equal(leg.legval(x, [1, 0]).shape, dims)
+            assert_equal(leg.legval(x, [1, 0, 0]).shape, dims)
+
+    def test_legval2d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test exceptions
+        assert_raises(ValueError, leg.legval2d, x1, x2[:2], self.c2d)
+
+        #test values
+        tgt = y1*y2
+        res = leg.legval2d(x1, x2, self.c2d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = leg.legval2d(z, z, self.c2d)
+        assert_(res.shape == (2, 3))
+
+    def test_legval3d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test exceptions
+        assert_raises(ValueError, leg.legval3d, x1, x2, x3[:2], self.c3d)
+
+        #test values
+        tgt = y1*y2*y3
+        res = leg.legval3d(x1, x2, x3, self.c3d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = leg.legval3d(z, z, z, self.c3d)
+        assert_(res.shape == (2, 3))
+
+    def test_leggrid2d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test values
+        tgt = np.einsum('i,j->ij', y1, y2)
+        res = leg.leggrid2d(x1, x2, self.c2d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = leg.leggrid2d(z, z, self.c2d)
+        assert_(res.shape == (2, 3)*2)
+
+    def test_leggrid3d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test values
+        tgt = np.einsum('i,j,k->ijk', y1, y2, y3)
+        res = leg.leggrid3d(x1, x2, x3, self.c3d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = leg.leggrid3d(z, z, z, self.c3d)
+        assert_(res.shape == (2, 3)*3)
+
+
+class TestIntegral:
+
+    def test_legint(self):
+        # check exceptions
+        assert_raises(TypeError, leg.legint, [0], .5)
+        assert_raises(ValueError, leg.legint, [0], -1)
+        assert_raises(ValueError, leg.legint, [0], 1, [0, 0])
+        assert_raises(ValueError, leg.legint, [0], lbnd=[0])
+        assert_raises(ValueError, leg.legint, [0], scl=[0])
+        assert_raises(TypeError, leg.legint, [0], axis=.5)
+
+        # test integration of zero polynomial
+        for i in range(2, 5):
+            k = [0]*(i - 2) + [1]
+            res = leg.legint([0], m=i, k=k)
+            assert_almost_equal(res, [0, 1])
+
+        # check single integration with integration constant
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            tgt = [i] + [0]*i + [1/scl]
+            legpol = leg.poly2leg(pol)
+            legint = leg.legint(legpol, m=1, k=[i])
+            res = leg.leg2poly(legint)
+            assert_almost_equal(trim(res), trim(tgt))
+
+        # check single integration with integration constant and lbnd
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            legpol = leg.poly2leg(pol)
+            legint = leg.legint(legpol, m=1, k=[i], lbnd=-1)
+            assert_almost_equal(leg.legval(-1, legint), i)
+
+        # check single integration with integration constant and scaling
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            tgt = [i] + [0]*i + [2/scl]
+            legpol = leg.poly2leg(pol)
+            legint = leg.legint(legpol, m=1, k=[i], scl=2)
+            res = leg.leg2poly(legint)
+            assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with default k
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = leg.legint(tgt, m=1)
+                res = leg.legint(pol, m=j)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with defined k
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = leg.legint(tgt, m=1, k=[k])
+                res = leg.legint(pol, m=j, k=list(range(j)))
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with lbnd
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = leg.legint(tgt, m=1, k=[k], lbnd=-1)
+                res = leg.legint(pol, m=j, k=list(range(j)), lbnd=-1)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with scaling
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = leg.legint(tgt, m=1, k=[k], scl=2)
+                res = leg.legint(pol, m=j, k=list(range(j)), scl=2)
+                assert_almost_equal(trim(res), trim(tgt))
+
+    def test_legint_axis(self):
+        # check that axis keyword works
+        c2d = np.random.random((3, 4))
+
+        tgt = np.vstack([leg.legint(c) for c in c2d.T]).T
+        res = leg.legint(c2d, axis=0)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([leg.legint(c) for c in c2d])
+        res = leg.legint(c2d, axis=1)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([leg.legint(c, k=3) for c in c2d])
+        res = leg.legint(c2d, k=3, axis=1)
+        assert_almost_equal(res, tgt)
+
+    def test_legint_zerointord(self):
+        assert_equal(leg.legint((1, 2, 3), 0), (1, 2, 3))
+
+
+class TestDerivative:
+
+    def test_legder(self):
+        # check exceptions
+        assert_raises(TypeError, leg.legder, [0], .5)
+        assert_raises(ValueError, leg.legder, [0], -1)
+
+        # check that zeroth derivative does nothing
+        for i in range(5):
+            tgt = [0]*i + [1]
+            res = leg.legder(tgt, m=0)
+            assert_equal(trim(res), trim(tgt))
+
+        # check that derivation is the inverse of integration
+        for i in range(5):
+            for j in range(2, 5):
+                tgt = [0]*i + [1]
+                res = leg.legder(leg.legint(tgt, m=j), m=j)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check derivation with scaling
+        for i in range(5):
+            for j in range(2, 5):
+                tgt = [0]*i + [1]
+                res = leg.legder(leg.legint(tgt, m=j, scl=2), m=j, scl=.5)
+                assert_almost_equal(trim(res), trim(tgt))
+
+    def test_legder_axis(self):
+        # check that axis keyword works
+        c2d = np.random.random((3, 4))
+
+        tgt = np.vstack([leg.legder(c) for c in c2d.T]).T
+        res = leg.legder(c2d, axis=0)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([leg.legder(c) for c in c2d])
+        res = leg.legder(c2d, axis=1)
+        assert_almost_equal(res, tgt)
+
+    def test_legder_orderhigherthancoeff(self):
+        c = (1, 2, 3, 4)
+        assert_equal(leg.legder(c, 4), [0])
+
+class TestVander:
+    # some random values in [-1, 1)
+    x = np.random.random((3, 5))*2 - 1
+
+    def test_legvander(self):
+        # check for 1d x
+        x = np.arange(3)
+        v = leg.legvander(x, 3)
+        assert_(v.shape == (3, 4))
+        for i in range(4):
+            coef = [0]*i + [1]
+            assert_almost_equal(v[..., i], leg.legval(x, coef))
+
+        # check for 2d x
+        x = np.array([[1, 2], [3, 4], [5, 6]])
+        v = leg.legvander(x, 3)
+        assert_(v.shape == (3, 2, 4))
+        for i in range(4):
+            coef = [0]*i + [1]
+            assert_almost_equal(v[..., i], leg.legval(x, coef))
+
+    def test_legvander2d(self):
+        # also tests polyval2d for non-square coefficient array
+        x1, x2, x3 = self.x
+        c = np.random.random((2, 3))
+        van = leg.legvander2d(x1, x2, [1, 2])
+        tgt = leg.legval2d(x1, x2, c)
+        res = np.dot(van, c.flat)
+        assert_almost_equal(res, tgt)
+
+        # check shape
+        van = leg.legvander2d([x1], [x2], [1, 2])
+        assert_(van.shape == (1, 5, 6))
+
+    def test_legvander3d(self):
+        # also tests polyval3d for non-square coefficient array
+        x1, x2, x3 = self.x
+        c = np.random.random((2, 3, 4))
+        van = leg.legvander3d(x1, x2, x3, [1, 2, 3])
+        tgt = leg.legval3d(x1, x2, x3, c)
+        res = np.dot(van, c.flat)
+        assert_almost_equal(res, tgt)
+
+        # check shape
+        van = leg.legvander3d([x1], [x2], [x3], [1, 2, 3])
+        assert_(van.shape == (1, 5, 24))
+
+    def test_legvander_negdeg(self):
+        assert_raises(ValueError, leg.legvander, (1, 2, 3), -1)
+
+
+class TestFitting:
+
+    def test_legfit(self):
+        def f(x):
+            return x*(x - 1)*(x - 2)
+
+        def f2(x):
+            return x**4 + x**2 + 1
+
+        # Test exceptions
+        assert_raises(ValueError, leg.legfit, [1], [1], -1)
+        assert_raises(TypeError, leg.legfit, [[1]], [1], 0)
+        assert_raises(TypeError, leg.legfit, [], [1], 0)
+        assert_raises(TypeError, leg.legfit, [1], [[[1]]], 0)
+        assert_raises(TypeError, leg.legfit, [1, 2], [1], 0)
+        assert_raises(TypeError, leg.legfit, [1], [1, 2], 0)
+        assert_raises(TypeError, leg.legfit, [1], [1], 0, w=[[1]])
+        assert_raises(TypeError, leg.legfit, [1], [1], 0, w=[1, 1])
+        assert_raises(ValueError, leg.legfit, [1], [1], [-1,])
+        assert_raises(ValueError, leg.legfit, [1], [1], [2, -1, 6])
+        assert_raises(TypeError, leg.legfit, [1], [1], [])
+
+        # Test fit
+        x = np.linspace(0, 2)
+        y = f(x)
+        #
+        coef3 = leg.legfit(x, y, 3)
+        assert_equal(len(coef3), 4)
+        assert_almost_equal(leg.legval(x, coef3), y)
+        coef3 = leg.legfit(x, y, [0, 1, 2, 3])
+        assert_equal(len(coef3), 4)
+        assert_almost_equal(leg.legval(x, coef3), y)
+        #
+        coef4 = leg.legfit(x, y, 4)
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(leg.legval(x, coef4), y)
+        coef4 = leg.legfit(x, y, [0, 1, 2, 3, 4])
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(leg.legval(x, coef4), y)
+        # check things still work if deg is not in strict increasing
+        coef4 = leg.legfit(x, y, [2, 3, 4, 1, 0])
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(leg.legval(x, coef4), y)
+        #
+        coef2d = leg.legfit(x, np.array([y, y]).T, 3)
+        assert_almost_equal(coef2d, np.array([coef3, coef3]).T)
+        coef2d = leg.legfit(x, np.array([y, y]).T, [0, 1, 2, 3])
+        assert_almost_equal(coef2d, np.array([coef3, coef3]).T)
+        # test weighting
+        w = np.zeros_like(x)
+        yw = y.copy()
+        w[1::2] = 1
+        y[0::2] = 0
+        wcoef3 = leg.legfit(x, yw, 3, w=w)
+        assert_almost_equal(wcoef3, coef3)
+        wcoef3 = leg.legfit(x, yw, [0, 1, 2, 3], w=w)
+        assert_almost_equal(wcoef3, coef3)
+        #
+        wcoef2d = leg.legfit(x, np.array([yw, yw]).T, 3, w=w)
+        assert_almost_equal(wcoef2d, np.array([coef3, coef3]).T)
+        wcoef2d = leg.legfit(x, np.array([yw, yw]).T, [0, 1, 2, 3], w=w)
+        assert_almost_equal(wcoef2d, np.array([coef3, coef3]).T)
+        # test scaling with complex values x points whose square
+        # is zero when summed.
+        x = [1, 1j, -1, -1j]
+        assert_almost_equal(leg.legfit(x, x, 1), [0, 1])
+        assert_almost_equal(leg.legfit(x, x, [0, 1]), [0, 1])
+        # test fitting only even Legendre polynomials
+        x = np.linspace(-1, 1)
+        y = f2(x)
+        coef1 = leg.legfit(x, y, 4)
+        assert_almost_equal(leg.legval(x, coef1), y)
+        coef2 = leg.legfit(x, y, [0, 2, 4])
+        assert_almost_equal(leg.legval(x, coef2), y)
+        assert_almost_equal(coef1, coef2)
+
+
+class TestCompanion:
+
+    def test_raises(self):
+        assert_raises(ValueError, leg.legcompanion, [])
+        assert_raises(ValueError, leg.legcompanion, [1])
+
+    def test_dimensions(self):
+        for i in range(1, 5):
+            coef = [0]*i + [1]
+            assert_(leg.legcompanion(coef).shape == (i, i))
+
+    def test_linear_root(self):
+        assert_(leg.legcompanion([1, 2])[0, 0] == -.5)
+
+
+class TestGauss:
+
+    def test_100(self):
+        x, w = leg.leggauss(100)
+
+        # test orthogonality. Note that the results need to be normalized,
+        # otherwise the huge values that can arise from fast growing
+        # functions like Laguerre can be very confusing.
+        v = leg.legvander(x, 99)
+        vv = np.dot(v.T * w, v)
+        vd = 1/np.sqrt(vv.diagonal())
+        vv = vd[:, None] * vv * vd
+        assert_almost_equal(vv, np.eye(100))
+
+        # check that the integral of 1 is correct
+        tgt = 2.0
+        assert_almost_equal(w.sum(), tgt)
+
+
+class TestMisc:
+
+    def test_legfromroots(self):
+        res = leg.legfromroots([])
+        assert_almost_equal(trim(res), [1])
+        for i in range(1, 5):
+            roots = np.cos(np.linspace(-np.pi, 0, 2*i + 1)[1::2])
+            pol = leg.legfromroots(roots)
+            res = leg.legval(roots, pol)
+            tgt = 0
+            assert_(len(pol) == i + 1)
+            assert_almost_equal(leg.leg2poly(pol)[-1], 1)
+            assert_almost_equal(res, tgt)
+
+    def test_legroots(self):
+        assert_almost_equal(leg.legroots([1]), [])
+        assert_almost_equal(leg.legroots([1, 2]), [-.5])
+        for i in range(2, 5):
+            tgt = np.linspace(-1, 1, i)
+            res = leg.legroots(leg.legfromroots(tgt))
+            assert_almost_equal(trim(res), trim(tgt))
+
+    def test_legtrim(self):
+        coef = [2, -1, 1, 0]
+
+        # Test exceptions
+        assert_raises(ValueError, leg.legtrim, coef, -1)
+
+        # Test results
+        assert_equal(leg.legtrim(coef), coef[:-1])
+        assert_equal(leg.legtrim(coef, 1), coef[:-3])
+        assert_equal(leg.legtrim(coef, 2), [0])
+
+    def test_legline(self):
+        assert_equal(leg.legline(3, 4), [3, 4])
+
+    def test_legline_zeroscl(self):
+        assert_equal(leg.legline(3, 0), [3])
+
+    def test_leg2poly(self):
+        for i in range(10):
+            assert_almost_equal(leg.leg2poly([0]*i + [1]), Llist[i])
+
+    def test_poly2leg(self):
+        for i in range(10):
+            assert_almost_equal(leg.poly2leg(Llist[i]), [0]*i + [1])
+
+    def test_weight(self):
+        x = np.linspace(-1, 1, 11)
+        tgt = 1.
+        res = leg.legweight(x)
+        assert_almost_equal(res, tgt)
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_polynomial.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_polynomial.py
new file mode 100644
index 00000000..6b3ef238
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_polynomial.py
@@ -0,0 +1,611 @@
+"""Tests for polynomial module.
+
+"""
+from functools import reduce
+
+import numpy as np
+import numpy.polynomial.polynomial as poly
+import pickle
+from copy import deepcopy
+from numpy.testing import (
+    assert_almost_equal, assert_raises, assert_equal, assert_,
+    assert_warns, assert_array_equal, assert_raises_regex)
+
+
+def trim(x):
+    return poly.polytrim(x, tol=1e-6)
+
+T0 = [1]
+T1 = [0, 1]
+T2 = [-1, 0, 2]
+T3 = [0, -3, 0, 4]
+T4 = [1, 0, -8, 0, 8]
+T5 = [0, 5, 0, -20, 0, 16]
+T6 = [-1, 0, 18, 0, -48, 0, 32]
+T7 = [0, -7, 0, 56, 0, -112, 0, 64]
+T8 = [1, 0, -32, 0, 160, 0, -256, 0, 128]
+T9 = [0, 9, 0, -120, 0, 432, 0, -576, 0, 256]
+
+Tlist = [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9]
+
+
+class TestConstants:
+
+    def test_polydomain(self):
+        assert_equal(poly.polydomain, [-1, 1])
+
+    def test_polyzero(self):
+        assert_equal(poly.polyzero, [0])
+
+    def test_polyone(self):
+        assert_equal(poly.polyone, [1])
+
+    def test_polyx(self):
+        assert_equal(poly.polyx, [0, 1])
+
+    def test_copy(self):
+        x = poly.Polynomial([1, 2, 3])
+        y = deepcopy(x)
+        assert_equal(x, y)
+
+    def test_pickle(self):
+        x = poly.Polynomial([1, 2, 3])
+        y = pickle.loads(pickle.dumps(x))
+        assert_equal(x, y)
+
+class TestArithmetic:
+
+    def test_polyadd(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(max(i, j) + 1)
+                tgt[i] += 1
+                tgt[j] += 1
+                res = poly.polyadd([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_polysub(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(max(i, j) + 1)
+                tgt[i] += 1
+                tgt[j] -= 1
+                res = poly.polysub([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_polymulx(self):
+        assert_equal(poly.polymulx([0]), [0])
+        assert_equal(poly.polymulx([1]), [0, 1])
+        for i in range(1, 5):
+            ser = [0]*i + [1]
+            tgt = [0]*(i + 1) + [1]
+            assert_equal(poly.polymulx(ser), tgt)
+
+    def test_polymul(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                tgt = np.zeros(i + j + 1)
+                tgt[i + j] += 1
+                res = poly.polymul([0]*i + [1], [0]*j + [1])
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+    def test_polydiv(self):
+        # check zero division
+        assert_raises(ZeroDivisionError, poly.polydiv, [1], [0])
+
+        # check scalar division
+        quo, rem = poly.polydiv([2], [2])
+        assert_equal((quo, rem), (1, 0))
+        quo, rem = poly.polydiv([2, 2], [2])
+        assert_equal((quo, rem), ((1, 1), 0))
+
+        # check rest.
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                ci = [0]*i + [1, 2]
+                cj = [0]*j + [1, 2]
+                tgt = poly.polyadd(ci, cj)
+                quo, rem = poly.polydiv(tgt, ci)
+                res = poly.polyadd(poly.polymul(quo, ci), rem)
+                assert_equal(res, tgt, err_msg=msg)
+
+    def test_polypow(self):
+        for i in range(5):
+            for j in range(5):
+                msg = f"At i={i}, j={j}"
+                c = np.arange(i + 1)
+                tgt = reduce(poly.polymul, [c]*j, np.array([1]))
+                res = poly.polypow(c, j) 
+                assert_equal(trim(res), trim(tgt), err_msg=msg)
+
+
+class TestEvaluation:
+    # coefficients of 1 + 2*x + 3*x**2
+    c1d = np.array([1., 2., 3.])
+    c2d = np.einsum('i,j->ij', c1d, c1d)
+    c3d = np.einsum('i,j,k->ijk', c1d, c1d, c1d)
+
+    # some random values in [-1, 1)
+    x = np.random.random((3, 5))*2 - 1
+    y = poly.polyval(x, [1., 2., 3.])
+
+    def test_polyval(self):
+        #check empty input
+        assert_equal(poly.polyval([], [1]).size, 0)
+
+        #check normal input)
+        x = np.linspace(-1, 1)
+        y = [x**i for i in range(5)]
+        for i in range(5):
+            tgt = y[i]
+            res = poly.polyval(x, [0]*i + [1])
+            assert_almost_equal(res, tgt)
+        tgt = x*(x**2 - 1)
+        res = poly.polyval(x, [0, -1, 0, 1])
+        assert_almost_equal(res, tgt)
+
+        #check that shape is preserved
+        for i in range(3):
+            dims = [2]*i
+            x = np.zeros(dims)
+            assert_equal(poly.polyval(x, [1]).shape, dims)
+            assert_equal(poly.polyval(x, [1, 0]).shape, dims)
+            assert_equal(poly.polyval(x, [1, 0, 0]).shape, dims)
+
+        #check masked arrays are processed correctly
+        mask = [False, True, False]
+        mx = np.ma.array([1, 2, 3], mask=mask)
+        res = np.polyval([7, 5, 3], mx)
+        assert_array_equal(res.mask, mask)
+
+        #check subtypes of ndarray are preserved
+        class C(np.ndarray):
+            pass
+
+        cx = np.array([1, 2, 3]).view(C)
+        assert_equal(type(np.polyval([2, 3, 4], cx)), C)
+
+    def test_polyvalfromroots(self):
+        # check exception for broadcasting x values over root array with
+        # too few dimensions
+        assert_raises(ValueError, poly.polyvalfromroots,
+                      [1], [1], tensor=False)
+
+        # check empty input
+        assert_equal(poly.polyvalfromroots([], [1]).size, 0)
+        assert_(poly.polyvalfromroots([], [1]).shape == (0,))
+
+        # check empty input + multidimensional roots
+        assert_equal(poly.polyvalfromroots([], [[1] * 5]).size, 0)
+        assert_(poly.polyvalfromroots([], [[1] * 5]).shape == (5, 0))
+
+        # check scalar input
+        assert_equal(poly.polyvalfromroots(1, 1), 0)
+        assert_(poly.polyvalfromroots(1, np.ones((3, 3))).shape == (3,))
+
+        # check normal input)
+        x = np.linspace(-1, 1)
+        y = [x**i for i in range(5)]
+        for i in range(1, 5):
+            tgt = y[i]
+            res = poly.polyvalfromroots(x, [0]*i)
+            assert_almost_equal(res, tgt)
+        tgt = x*(x - 1)*(x + 1)
+        res = poly.polyvalfromroots(x, [-1, 0, 1])
+        assert_almost_equal(res, tgt)
+
+        # check that shape is preserved
+        for i in range(3):
+            dims = [2]*i
+            x = np.zeros(dims)
+            assert_equal(poly.polyvalfromroots(x, [1]).shape, dims)
+            assert_equal(poly.polyvalfromroots(x, [1, 0]).shape, dims)
+            assert_equal(poly.polyvalfromroots(x, [1, 0, 0]).shape, dims)
+
+        # check compatibility with factorization
+        ptest = [15, 2, -16, -2, 1]
+        r = poly.polyroots(ptest)
+        x = np.linspace(-1, 1)
+        assert_almost_equal(poly.polyval(x, ptest),
+                            poly.polyvalfromroots(x, r))
+
+        # check multidimensional arrays of roots and values
+        # check tensor=False
+        rshape = (3, 5)
+        x = np.arange(-3, 2)
+        r = np.random.randint(-5, 5, size=rshape)
+        res = poly.polyvalfromroots(x, r, tensor=False)
+        tgt = np.empty(r.shape[1:])
+        for ii in range(tgt.size):
+            tgt[ii] = poly.polyvalfromroots(x[ii], r[:, ii])
+        assert_equal(res, tgt)
+
+        # check tensor=True
+        x = np.vstack([x, 2*x])
+        res = poly.polyvalfromroots(x, r, tensor=True)
+        tgt = np.empty(r.shape[1:] + x.shape)
+        for ii in range(r.shape[1]):
+            for jj in range(x.shape[0]):
+                tgt[ii, jj, :] = poly.polyvalfromroots(x[jj], r[:, ii])
+        assert_equal(res, tgt)
+
+    def test_polyval2d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test exceptions
+        assert_raises_regex(ValueError, 'incompatible',
+                            poly.polyval2d, x1, x2[:2], self.c2d)
+
+        #test values
+        tgt = y1*y2
+        res = poly.polyval2d(x1, x2, self.c2d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = poly.polyval2d(z, z, self.c2d)
+        assert_(res.shape == (2, 3))
+
+    def test_polyval3d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test exceptions
+        assert_raises_regex(ValueError, 'incompatible',
+                      poly.polyval3d, x1, x2, x3[:2], self.c3d)
+
+        #test values
+        tgt = y1*y2*y3
+        res = poly.polyval3d(x1, x2, x3, self.c3d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = poly.polyval3d(z, z, z, self.c3d)
+        assert_(res.shape == (2, 3))
+
+    def test_polygrid2d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test values
+        tgt = np.einsum('i,j->ij', y1, y2)
+        res = poly.polygrid2d(x1, x2, self.c2d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = poly.polygrid2d(z, z, self.c2d)
+        assert_(res.shape == (2, 3)*2)
+
+    def test_polygrid3d(self):
+        x1, x2, x3 = self.x
+        y1, y2, y3 = self.y
+
+        #test values
+        tgt = np.einsum('i,j,k->ijk', y1, y2, y3)
+        res = poly.polygrid3d(x1, x2, x3, self.c3d)
+        assert_almost_equal(res, tgt)
+
+        #test shape
+        z = np.ones((2, 3))
+        res = poly.polygrid3d(z, z, z, self.c3d)
+        assert_(res.shape == (2, 3)*3)
+
+
+class TestIntegral:
+
+    def test_polyint(self):
+        # check exceptions
+        assert_raises(TypeError, poly.polyint, [0], .5)
+        assert_raises(ValueError, poly.polyint, [0], -1)
+        assert_raises(ValueError, poly.polyint, [0], 1, [0, 0])
+        assert_raises(ValueError, poly.polyint, [0], lbnd=[0])
+        assert_raises(ValueError, poly.polyint, [0], scl=[0])
+        assert_raises(TypeError, poly.polyint, [0], axis=.5)
+        with assert_warns(DeprecationWarning):
+            poly.polyint([1, 1], 1.)
+
+        # test integration of zero polynomial
+        for i in range(2, 5):
+            k = [0]*(i - 2) + [1]
+            res = poly.polyint([0], m=i, k=k)
+            assert_almost_equal(res, [0, 1])
+
+        # check single integration with integration constant
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            tgt = [i] + [0]*i + [1/scl]
+            res = poly.polyint(pol, m=1, k=[i])
+            assert_almost_equal(trim(res), trim(tgt))
+
+        # check single integration with integration constant and lbnd
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            res = poly.polyint(pol, m=1, k=[i], lbnd=-1)
+            assert_almost_equal(poly.polyval(-1, res), i)
+
+        # check single integration with integration constant and scaling
+        for i in range(5):
+            scl = i + 1
+            pol = [0]*i + [1]
+            tgt = [i] + [0]*i + [2/scl]
+            res = poly.polyint(pol, m=1, k=[i], scl=2)
+            assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with default k
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = poly.polyint(tgt, m=1)
+                res = poly.polyint(pol, m=j)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with defined k
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = poly.polyint(tgt, m=1, k=[k])
+                res = poly.polyint(pol, m=j, k=list(range(j)))
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with lbnd
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = poly.polyint(tgt, m=1, k=[k], lbnd=-1)
+                res = poly.polyint(pol, m=j, k=list(range(j)), lbnd=-1)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check multiple integrations with scaling
+        for i in range(5):
+            for j in range(2, 5):
+                pol = [0]*i + [1]
+                tgt = pol[:]
+                for k in range(j):
+                    tgt = poly.polyint(tgt, m=1, k=[k], scl=2)
+                res = poly.polyint(pol, m=j, k=list(range(j)), scl=2)
+                assert_almost_equal(trim(res), trim(tgt))
+
+    def test_polyint_axis(self):
+        # check that axis keyword works
+        c2d = np.random.random((3, 4))
+
+        tgt = np.vstack([poly.polyint(c) for c in c2d.T]).T
+        res = poly.polyint(c2d, axis=0)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([poly.polyint(c) for c in c2d])
+        res = poly.polyint(c2d, axis=1)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([poly.polyint(c, k=3) for c in c2d])
+        res = poly.polyint(c2d, k=3, axis=1)
+        assert_almost_equal(res, tgt)
+
+
+class TestDerivative:
+
+    def test_polyder(self):
+        # check exceptions
+        assert_raises(TypeError, poly.polyder, [0], .5)
+        assert_raises(ValueError, poly.polyder, [0], -1)
+
+        # check that zeroth derivative does nothing
+        for i in range(5):
+            tgt = [0]*i + [1]
+            res = poly.polyder(tgt, m=0)
+            assert_equal(trim(res), trim(tgt))
+
+        # check that derivation is the inverse of integration
+        for i in range(5):
+            for j in range(2, 5):
+                tgt = [0]*i + [1]
+                res = poly.polyder(poly.polyint(tgt, m=j), m=j)
+                assert_almost_equal(trim(res), trim(tgt))
+
+        # check derivation with scaling
+        for i in range(5):
+            for j in range(2, 5):
+                tgt = [0]*i + [1]
+                res = poly.polyder(poly.polyint(tgt, m=j, scl=2), m=j, scl=.5)
+                assert_almost_equal(trim(res), trim(tgt))
+
+    def test_polyder_axis(self):
+        # check that axis keyword works
+        c2d = np.random.random((3, 4))
+
+        tgt = np.vstack([poly.polyder(c) for c in c2d.T]).T
+        res = poly.polyder(c2d, axis=0)
+        assert_almost_equal(res, tgt)
+
+        tgt = np.vstack([poly.polyder(c) for c in c2d])
+        res = poly.polyder(c2d, axis=1)
+        assert_almost_equal(res, tgt)
+
+
+class TestVander:
+    # some random values in [-1, 1)
+    x = np.random.random((3, 5))*2 - 1
+
+    def test_polyvander(self):
+        # check for 1d x
+        x = np.arange(3)
+        v = poly.polyvander(x, 3)
+        assert_(v.shape == (3, 4))
+        for i in range(4):
+            coef = [0]*i + [1]
+            assert_almost_equal(v[..., i], poly.polyval(x, coef))
+
+        # check for 2d x
+        x = np.array([[1, 2], [3, 4], [5, 6]])
+        v = poly.polyvander(x, 3)
+        assert_(v.shape == (3, 2, 4))
+        for i in range(4):
+            coef = [0]*i + [1]
+            assert_almost_equal(v[..., i], poly.polyval(x, coef))
+
+    def test_polyvander2d(self):
+        # also tests polyval2d for non-square coefficient array
+        x1, x2, x3 = self.x
+        c = np.random.random((2, 3))
+        van = poly.polyvander2d(x1, x2, [1, 2])
+        tgt = poly.polyval2d(x1, x2, c)
+        res = np.dot(van, c.flat)
+        assert_almost_equal(res, tgt)
+
+        # check shape
+        van = poly.polyvander2d([x1], [x2], [1, 2])
+        assert_(van.shape == (1, 5, 6))
+
+    def test_polyvander3d(self):
+        # also tests polyval3d for non-square coefficient array
+        x1, x2, x3 = self.x
+        c = np.random.random((2, 3, 4))
+        van = poly.polyvander3d(x1, x2, x3, [1, 2, 3])
+        tgt = poly.polyval3d(x1, x2, x3, c)
+        res = np.dot(van, c.flat)
+        assert_almost_equal(res, tgt)
+
+        # check shape
+        van = poly.polyvander3d([x1], [x2], [x3], [1, 2, 3])
+        assert_(van.shape == (1, 5, 24))
+
+    def test_polyvandernegdeg(self):
+        x = np.arange(3)
+        assert_raises(ValueError, poly.polyvander, x, -1)
+
+
+class TestCompanion:
+
+    def test_raises(self):
+        assert_raises(ValueError, poly.polycompanion, [])
+        assert_raises(ValueError, poly.polycompanion, [1])
+
+    def test_dimensions(self):
+        for i in range(1, 5):
+            coef = [0]*i + [1]
+            assert_(poly.polycompanion(coef).shape == (i, i))
+
+    def test_linear_root(self):
+        assert_(poly.polycompanion([1, 2])[0, 0] == -.5)
+
+
+class TestMisc:
+
+    def test_polyfromroots(self):
+        res = poly.polyfromroots([])
+        assert_almost_equal(trim(res), [1])
+        for i in range(1, 5):
+            roots = np.cos(np.linspace(-np.pi, 0, 2*i + 1)[1::2])
+            tgt = Tlist[i]
+            res = poly.polyfromroots(roots)*2**(i-1)
+            assert_almost_equal(trim(res), trim(tgt))
+
+    def test_polyroots(self):
+        assert_almost_equal(poly.polyroots([1]), [])
+        assert_almost_equal(poly.polyroots([1, 2]), [-.5])
+        for i in range(2, 5):
+            tgt = np.linspace(-1, 1, i)
+            res = poly.polyroots(poly.polyfromroots(tgt))
+            assert_almost_equal(trim(res), trim(tgt))
+
+    def test_polyfit(self):
+        def f(x):
+            return x*(x - 1)*(x - 2)
+
+        def f2(x):
+            return x**4 + x**2 + 1
+
+        # Test exceptions
+        assert_raises(ValueError, poly.polyfit, [1], [1], -1)
+        assert_raises(TypeError, poly.polyfit, [[1]], [1], 0)
+        assert_raises(TypeError, poly.polyfit, [], [1], 0)
+        assert_raises(TypeError, poly.polyfit, [1], [[[1]]], 0)
+        assert_raises(TypeError, poly.polyfit, [1, 2], [1], 0)
+        assert_raises(TypeError, poly.polyfit, [1], [1, 2], 0)
+        assert_raises(TypeError, poly.polyfit, [1], [1], 0, w=[[1]])
+        assert_raises(TypeError, poly.polyfit, [1], [1], 0, w=[1, 1])
+        assert_raises(ValueError, poly.polyfit, [1], [1], [-1,])
+        assert_raises(ValueError, poly.polyfit, [1], [1], [2, -1, 6])
+        assert_raises(TypeError, poly.polyfit, [1], [1], [])
+
+        # Test fit
+        x = np.linspace(0, 2)
+        y = f(x)
+        #
+        coef3 = poly.polyfit(x, y, 3)
+        assert_equal(len(coef3), 4)
+        assert_almost_equal(poly.polyval(x, coef3), y)
+        coef3 = poly.polyfit(x, y, [0, 1, 2, 3])
+        assert_equal(len(coef3), 4)
+        assert_almost_equal(poly.polyval(x, coef3), y)
+        #
+        coef4 = poly.polyfit(x, y, 4)
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(poly.polyval(x, coef4), y)
+        coef4 = poly.polyfit(x, y, [0, 1, 2, 3, 4])
+        assert_equal(len(coef4), 5)
+        assert_almost_equal(poly.polyval(x, coef4), y)
+        #
+        coef2d = poly.polyfit(x, np.array([y, y]).T, 3)
+        assert_almost_equal(coef2d, np.array([coef3, coef3]).T)
+        coef2d = poly.polyfit(x, np.array([y, y]).T, [0, 1, 2, 3])
+        assert_almost_equal(coef2d, np.array([coef3, coef3]).T)
+        # test weighting
+        w = np.zeros_like(x)
+        yw = y.copy()
+        w[1::2] = 1
+        yw[0::2] = 0
+        wcoef3 = poly.polyfit(x, yw, 3, w=w)
+        assert_almost_equal(wcoef3, coef3)
+        wcoef3 = poly.polyfit(x, yw, [0, 1, 2, 3], w=w)
+        assert_almost_equal(wcoef3, coef3)
+        #
+        wcoef2d = poly.polyfit(x, np.array([yw, yw]).T, 3, w=w)
+        assert_almost_equal(wcoef2d, np.array([coef3, coef3]).T)
+        wcoef2d = poly.polyfit(x, np.array([yw, yw]).T, [0, 1, 2, 3], w=w)
+        assert_almost_equal(wcoef2d, np.array([coef3, coef3]).T)
+        # test scaling with complex values x points whose square
+        # is zero when summed.
+        x = [1, 1j, -1, -1j]
+        assert_almost_equal(poly.polyfit(x, x, 1), [0, 1])
+        assert_almost_equal(poly.polyfit(x, x, [0, 1]), [0, 1])
+        # test fitting only even Polyendre polynomials
+        x = np.linspace(-1, 1)
+        y = f2(x)
+        coef1 = poly.polyfit(x, y, 4)
+        assert_almost_equal(poly.polyval(x, coef1), y)
+        coef2 = poly.polyfit(x, y, [0, 2, 4])
+        assert_almost_equal(poly.polyval(x, coef2), y)
+        assert_almost_equal(coef1, coef2)
+
+    def test_polytrim(self):
+        coef = [2, -1, 1, 0]
+
+        # Test exceptions
+        assert_raises(ValueError, poly.polytrim, coef, -1)
+
+        # Test results
+        assert_equal(poly.polytrim(coef), coef[:-1])
+        assert_equal(poly.polytrim(coef, 1), coef[:-3])
+        assert_equal(poly.polytrim(coef, 2), [0])
+
+    def test_polyline(self):
+        assert_equal(poly.polyline(3, 4), [3, 4])
+
+    def test_polyline_zero(self):
+        assert_equal(poly.polyline(3, 0), [3])
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_polyutils.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_polyutils.py
new file mode 100644
index 00000000..cc630790
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_polyutils.py
@@ -0,0 +1,121 @@
+"""Tests for polyutils module.
+
+"""
+import numpy as np
+import numpy.polynomial.polyutils as pu
+from numpy.testing import (
+    assert_almost_equal, assert_raises, assert_equal, assert_,
+    )
+
+
+class TestMisc:
+
+    def test_trimseq(self):
+        for i in range(5):
+            tgt = [1]
+            res = pu.trimseq([1] + [0]*5)
+            assert_equal(res, tgt)
+
+    def test_as_series(self):
+        # check exceptions
+        assert_raises(ValueError, pu.as_series, [[]])
+        assert_raises(ValueError, pu.as_series, [[[1, 2]]])
+        assert_raises(ValueError, pu.as_series, [[1], ['a']])
+        # check common types
+        types = ['i', 'd', 'O']
+        for i in range(len(types)):
+            for j in range(i):
+                ci = np.ones(1, types[i])
+                cj = np.ones(1, types[j])
+                [resi, resj] = pu.as_series([ci, cj])
+                assert_(resi.dtype.char == resj.dtype.char)
+                assert_(resj.dtype.char == types[i])
+
+    def test_trimcoef(self):
+        coef = [2, -1, 1, 0]
+        # Test exceptions
+        assert_raises(ValueError, pu.trimcoef, coef, -1)
+        # Test results
+        assert_equal(pu.trimcoef(coef), coef[:-1])
+        assert_equal(pu.trimcoef(coef, 1), coef[:-3])
+        assert_equal(pu.trimcoef(coef, 2), [0])
+
+    def test_vander_nd_exception(self):
+        # n_dims != len(points)
+        assert_raises(ValueError, pu._vander_nd, (), (1, 2, 3), [90])
+        # n_dims != len(degrees)
+        assert_raises(ValueError, pu._vander_nd, (), (), [90.65])
+        # n_dims == 0
+        assert_raises(ValueError, pu._vander_nd, (), (), [])
+
+    def test_div_zerodiv(self):
+        # c2[-1] == 0
+        assert_raises(ZeroDivisionError, pu._div, pu._div, (1, 2, 3), [0])
+
+    def test_pow_too_large(self):
+        # power > maxpower
+        assert_raises(ValueError, pu._pow, (), [1, 2, 3], 5, 4)
+
+class TestDomain:
+
+    def test_getdomain(self):
+        # test for real values
+        x = [1, 10, 3, -1]
+        tgt = [-1, 10]
+        res = pu.getdomain(x)
+        assert_almost_equal(res, tgt)
+
+        # test for complex values
+        x = [1 + 1j, 1 - 1j, 0, 2]
+        tgt = [-1j, 2 + 1j]
+        res = pu.getdomain(x)
+        assert_almost_equal(res, tgt)
+
+    def test_mapdomain(self):
+        # test for real values
+        dom1 = [0, 4]
+        dom2 = [1, 3]
+        tgt = dom2
+        res = pu.mapdomain(dom1, dom1, dom2)
+        assert_almost_equal(res, tgt)
+
+        # test for complex values
+        dom1 = [0 - 1j, 2 + 1j]
+        dom2 = [-2, 2]
+        tgt = dom2
+        x = dom1
+        res = pu.mapdomain(x, dom1, dom2)
+        assert_almost_equal(res, tgt)
+
+        # test for multidimensional arrays
+        dom1 = [0, 4]
+        dom2 = [1, 3]
+        tgt = np.array([dom2, dom2])
+        x = np.array([dom1, dom1])
+        res = pu.mapdomain(x, dom1, dom2)
+        assert_almost_equal(res, tgt)
+
+        # test that subtypes are preserved.
+        class MyNDArray(np.ndarray):
+            pass
+
+        dom1 = [0, 4]
+        dom2 = [1, 3]
+        x = np.array([dom1, dom1]).view(MyNDArray)
+        res = pu.mapdomain(x, dom1, dom2)
+        assert_(isinstance(res, MyNDArray))
+
+    def test_mapparms(self):
+        # test for real values
+        dom1 = [0, 4]
+        dom2 = [1, 3]
+        tgt = [1, .5]
+        res = pu. mapparms(dom1, dom2)
+        assert_almost_equal(res, tgt)
+
+        # test for complex values
+        dom1 = [0 - 1j, 2 + 1j]
+        dom2 = [-2, 2]
+        tgt = [-1 + 1j, 1 - 1j]
+        res = pu.mapparms(dom1, dom2)
+        assert_almost_equal(res, tgt)
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_printing.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_printing.py
new file mode 100644
index 00000000..6f2a5092
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_printing.py
@@ -0,0 +1,530 @@
+from math import nan, inf
+import pytest
+from numpy.core import array, arange, printoptions
+import numpy.polynomial as poly
+from numpy.testing import assert_equal, assert_
+
+# For testing polynomial printing with object arrays
+from fractions import Fraction
+from decimal import Decimal
+
+
+class TestStrUnicodeSuperSubscripts:
+
+    @pytest.fixture(scope='class', autouse=True)
+    def use_unicode(self):
+        poly.set_default_printstyle('unicode')
+
+    @pytest.mark.parametrize(('inp', 'tgt'), (
+        ([1, 2, 3], "1.0 + 2.0·x + 3.0·x²"),
+        ([-1, 0, 3, -1], "-1.0 + 0.0·x + 3.0·x² - 1.0·x³"),
+        (arange(12), ("0.0 + 1.0·x + 2.0·x² + 3.0·x³ + 4.0·x⁴ + 5.0·x⁵ + "
+                      "6.0·x⁶ + 7.0·x⁷ +\n8.0·x⁸ + 9.0·x⁹ + 10.0·x¹⁰ + "
+                      "11.0·x¹¹")),
+    ))
+    def test_polynomial_str(self, inp, tgt):
+        res = str(poly.Polynomial(inp))
+        assert_equal(res, tgt)
+
+    @pytest.mark.parametrize(('inp', 'tgt'), (
+        ([1, 2, 3], "1.0 + 2.0·T₁(x) + 3.0·T₂(x)"),
+        ([-1, 0, 3, -1], "-1.0 + 0.0·T₁(x) + 3.0·T₂(x) - 1.0·T₃(x)"),
+        (arange(12), ("0.0 + 1.0·T₁(x) + 2.0·T₂(x) + 3.0·T₃(x) + 4.0·T₄(x) + "
+                      "5.0·T₅(x) +\n6.0·T₆(x) + 7.0·T₇(x) + 8.0·T₈(x) + "
+                      "9.0·T₉(x) + 10.0·T₁₀(x) + 11.0·T₁₁(x)")),
+    ))
+    def test_chebyshev_str(self, inp, tgt):
+        res = str(poly.Chebyshev(inp))
+        assert_equal(res, tgt)
+
+    @pytest.mark.parametrize(('inp', 'tgt'), (
+        ([1, 2, 3], "1.0 + 2.0·P₁(x) + 3.0·P₂(x)"),
+        ([-1, 0, 3, -1], "-1.0 + 0.0·P₁(x) + 3.0·P₂(x) - 1.0·P₃(x)"),
+        (arange(12), ("0.0 + 1.0·P₁(x) + 2.0·P₂(x) + 3.0·P₃(x) + 4.0·P₄(x) + "
+                      "5.0·P₅(x) +\n6.0·P₆(x) + 7.0·P₇(x) + 8.0·P₈(x) + "
+                      "9.0·P₉(x) + 10.0·P₁₀(x) + 11.0·P₁₁(x)")),
+    ))
+    def test_legendre_str(self, inp, tgt):
+        res = str(poly.Legendre(inp))
+        assert_equal(res, tgt)
+
+    @pytest.mark.parametrize(('inp', 'tgt'), (
+        ([1, 2, 3], "1.0 + 2.0·H₁(x) + 3.0·H₂(x)"),
+        ([-1, 0, 3, -1], "-1.0 + 0.0·H₁(x) + 3.0·H₂(x) - 1.0·H₃(x)"),
+        (arange(12), ("0.0 + 1.0·H₁(x) + 2.0·H₂(x) + 3.0·H₃(x) + 4.0·H₄(x) + "
+                      "5.0·H₅(x) +\n6.0·H₆(x) + 7.0·H₇(x) + 8.0·H₈(x) + "
+                      "9.0·H₉(x) + 10.0·H₁₀(x) + 11.0·H₁₁(x)")),
+    ))
+    def test_hermite_str(self, inp, tgt):
+        res = str(poly.Hermite(inp))
+        assert_equal(res, tgt)
+
+    @pytest.mark.parametrize(('inp', 'tgt'), (
+        ([1, 2, 3], "1.0 + 2.0·He₁(x) + 3.0·He₂(x)"),
+        ([-1, 0, 3, -1], "-1.0 + 0.0·He₁(x) + 3.0·He₂(x) - 1.0·He₃(x)"),
+        (arange(12), ("0.0 + 1.0·He₁(x) + 2.0·He₂(x) + 3.0·He₃(x) + "
+                      "4.0·He₄(x) + 5.0·He₅(x) +\n6.0·He₆(x) + 7.0·He₇(x) + "
+                      "8.0·He₈(x) + 9.0·He₉(x) + 10.0·He₁₀(x) +\n"
+                      "11.0·He₁₁(x)")),
+    ))
+    def test_hermiteE_str(self, inp, tgt):
+        res = str(poly.HermiteE(inp))
+        assert_equal(res, tgt)
+
+    @pytest.mark.parametrize(('inp', 'tgt'), (
+        ([1, 2, 3], "1.0 + 2.0·L₁(x) + 3.0·L₂(x)"),
+        ([-1, 0, 3, -1], "-1.0 + 0.0·L₁(x) + 3.0·L₂(x) - 1.0·L₃(x)"),
+        (arange(12), ("0.0 + 1.0·L₁(x) + 2.0·L₂(x) + 3.0·L₃(x) + 4.0·L₄(x) + "
+                      "5.0·L₅(x) +\n6.0·L₆(x) + 7.0·L₇(x) + 8.0·L₈(x) + "
+                      "9.0·L₉(x) + 10.0·L₁₀(x) + 11.0·L₁₁(x)")),
+    ))
+    def test_laguerre_str(self, inp, tgt):
+        res = str(poly.Laguerre(inp))
+        assert_equal(res, tgt)
+
+
+class TestStrAscii:
+
+    @pytest.fixture(scope='class', autouse=True)
+    def use_ascii(self):
+        poly.set_default_printstyle('ascii')
+
+    @pytest.mark.parametrize(('inp', 'tgt'), (
+        ([1, 2, 3], "1.0 + 2.0 x + 3.0 x**2"),
+        ([-1, 0, 3, -1], "-1.0 + 0.0 x + 3.0 x**2 - 1.0 x**3"),
+        (arange(12), ("0.0 + 1.0 x + 2.0 x**2 + 3.0 x**3 + 4.0 x**4 + "
+                      "5.0 x**5 + 6.0 x**6 +\n7.0 x**7 + 8.0 x**8 + "
+                      "9.0 x**9 + 10.0 x**10 + 11.0 x**11")),
+    ))
+    def test_polynomial_str(self, inp, tgt):
+        res = str(poly.Polynomial(inp))
+        assert_equal(res, tgt)
+
+    @pytest.mark.parametrize(('inp', 'tgt'), (
+        ([1, 2, 3], "1.0 + 2.0 T_1(x) + 3.0 T_2(x)"),
+        ([-1, 0, 3, -1], "-1.0 + 0.0 T_1(x) + 3.0 T_2(x) - 1.0 T_3(x)"),
+        (arange(12), ("0.0 + 1.0 T_1(x) + 2.0 T_2(x) + 3.0 T_3(x) + "
+                      "4.0 T_4(x) + 5.0 T_5(x) +\n6.0 T_6(x) + 7.0 T_7(x) + "
+                      "8.0 T_8(x) + 9.0 T_9(x) + 10.0 T_10(x) +\n"
+                      "11.0 T_11(x)")),
+    ))
+    def test_chebyshev_str(self, inp, tgt):
+        res = str(poly.Chebyshev(inp))
+        assert_equal(res, tgt)
+
+    @pytest.mark.parametrize(('inp', 'tgt'), (
+        ([1, 2, 3], "1.0 + 2.0 P_1(x) + 3.0 P_2(x)"),
+        ([-1, 0, 3, -1], "-1.0 + 0.0 P_1(x) + 3.0 P_2(x) - 1.0 P_3(x)"),
+        (arange(12), ("0.0 + 1.0 P_1(x) + 2.0 P_2(x) + 3.0 P_3(x) + "
+                      "4.0 P_4(x) + 5.0 P_5(x) +\n6.0 P_6(x) + 7.0 P_7(x) + "
+                      "8.0 P_8(x) + 9.0 P_9(x) + 10.0 P_10(x) +\n"
+                      "11.0 P_11(x)")),
+    ))
+    def test_legendre_str(self, inp, tgt):
+        res = str(poly.Legendre(inp))
+        assert_equal(res, tgt)
+
+    @pytest.mark.parametrize(('inp', 'tgt'), (
+        ([1, 2, 3], "1.0 + 2.0 H_1(x) + 3.0 H_2(x)"),
+        ([-1, 0, 3, -1], "-1.0 + 0.0 H_1(x) + 3.0 H_2(x) - 1.0 H_3(x)"),
+        (arange(12), ("0.0 + 1.0 H_1(x) + 2.0 H_2(x) + 3.0 H_3(x) + "
+                      "4.0 H_4(x) + 5.0 H_5(x) +\n6.0 H_6(x) + 7.0 H_7(x) + "
+                      "8.0 H_8(x) + 9.0 H_9(x) + 10.0 H_10(x) +\n"
+                      "11.0 H_11(x)")),
+    ))
+    def test_hermite_str(self, inp, tgt):
+        res = str(poly.Hermite(inp))
+        assert_equal(res, tgt)
+
+    @pytest.mark.parametrize(('inp', 'tgt'), (
+        ([1, 2, 3], "1.0 + 2.0 He_1(x) + 3.0 He_2(x)"),
+        ([-1, 0, 3, -1], "-1.0 + 0.0 He_1(x) + 3.0 He_2(x) - 1.0 He_3(x)"),
+        (arange(12), ("0.0 + 1.0 He_1(x) + 2.0 He_2(x) + 3.0 He_3(x) + "
+                      "4.0 He_4(x) +\n5.0 He_5(x) + 6.0 He_6(x) + "
+                      "7.0 He_7(x) + 8.0 He_8(x) + 9.0 He_9(x) +\n"
+                      "10.0 He_10(x) + 11.0 He_11(x)")),
+    ))
+    def test_hermiteE_str(self, inp, tgt):
+        res = str(poly.HermiteE(inp))
+        assert_equal(res, tgt)
+
+    @pytest.mark.parametrize(('inp', 'tgt'), (
+        ([1, 2, 3], "1.0 + 2.0 L_1(x) + 3.0 L_2(x)"),
+        ([-1, 0, 3, -1], "-1.0 + 0.0 L_1(x) + 3.0 L_2(x) - 1.0 L_3(x)"),
+        (arange(12), ("0.0 + 1.0 L_1(x) + 2.0 L_2(x) + 3.0 L_3(x) + "
+                      "4.0 L_4(x) + 5.0 L_5(x) +\n6.0 L_6(x) + 7.0 L_7(x) + "
+                      "8.0 L_8(x) + 9.0 L_9(x) + 10.0 L_10(x) +\n"
+                      "11.0 L_11(x)")),
+    ))
+    def test_laguerre_str(self, inp, tgt):
+        res = str(poly.Laguerre(inp))
+        assert_equal(res, tgt)
+
+
+class TestLinebreaking:
+
+    @pytest.fixture(scope='class', autouse=True)
+    def use_ascii(self):
+        poly.set_default_printstyle('ascii')
+
+    def test_single_line_one_less(self):
+        # With 'ascii' style, len(str(p)) is default linewidth - 1 (i.e. 74)
+        p = poly.Polynomial([12345678, 12345678, 12345678, 12345678, 123])
+        assert_equal(len(str(p)), 74)
+        assert_equal(str(p), (
+            '12345678.0 + 12345678.0 x + 12345678.0 x**2 + '
+            '12345678.0 x**3 + 123.0 x**4'
+        ))
+
+    def test_num_chars_is_linewidth(self):
+        # len(str(p)) == default linewidth == 75
+        p = poly.Polynomial([12345678, 12345678, 12345678, 12345678, 1234])
+        assert_equal(len(str(p)), 75)
+        assert_equal(str(p), (
+            '12345678.0 + 12345678.0 x + 12345678.0 x**2 + '
+            '12345678.0 x**3 +\n1234.0 x**4'
+        ))
+
+    def test_first_linebreak_multiline_one_less_than_linewidth(self):
+        # Multiline str where len(first_line) + len(next_term) == lw - 1 == 74
+        p = poly.Polynomial(
+                [12345678, 12345678, 12345678, 12345678, 1, 12345678]
+            )
+        assert_equal(len(str(p).split('\n')[0]), 74)
+        assert_equal(str(p), (
+            '12345678.0 + 12345678.0 x + 12345678.0 x**2 + '
+            '12345678.0 x**3 + 1.0 x**4 +\n12345678.0 x**5'
+        ))
+
+    def test_first_linebreak_multiline_on_linewidth(self):
+        # First line is one character longer than previous test
+        p = poly.Polynomial(
+                [12345678, 12345678, 12345678, 12345678.12, 1, 12345678]
+            )
+        assert_equal(str(p), (
+            '12345678.0 + 12345678.0 x + 12345678.0 x**2 + '
+            '12345678.12 x**3 +\n1.0 x**4 + 12345678.0 x**5'
+        ))
+
+    @pytest.mark.parametrize(('lw', 'tgt'), (
+        (75, ('0.0 + 10.0 x + 200.0 x**2 + 3000.0 x**3 + 40000.0 x**4 + '
+              '500000.0 x**5 +\n600000.0 x**6 + 70000.0 x**7 + 8000.0 x**8 + '
+              '900.0 x**9')),
+        (45, ('0.0 + 10.0 x + 200.0 x**2 + 3000.0 x**3 +\n40000.0 x**4 + '
+              '500000.0 x**5 +\n600000.0 x**6 + 70000.0 x**7 + 8000.0 x**8 +\n'
+              '900.0 x**9')),
+        (132, ('0.0 + 10.0 x + 200.0 x**2 + 3000.0 x**3 + 40000.0 x**4 + '
+               '500000.0 x**5 + 600000.0 x**6 + 70000.0 x**7 + 8000.0 x**8 + '
+               '900.0 x**9')),
+    ))
+    def test_linewidth_printoption(self, lw, tgt):
+        p = poly.Polynomial(
+            [0, 10, 200, 3000, 40000, 500000, 600000, 70000, 8000, 900]
+        )
+        with printoptions(linewidth=lw):
+            assert_equal(str(p), tgt)
+            for line in str(p).split('\n'):
+                assert_(len(line) < lw)
+
+
+def test_set_default_printoptions():
+    p = poly.Polynomial([1, 2, 3])
+    c = poly.Chebyshev([1, 2, 3])
+    poly.set_default_printstyle('ascii')
+    assert_equal(str(p), "1.0 + 2.0 x + 3.0 x**2")
+    assert_equal(str(c), "1.0 + 2.0 T_1(x) + 3.0 T_2(x)")
+    poly.set_default_printstyle('unicode')
+    assert_equal(str(p), "1.0 + 2.0·x + 3.0·x²")
+    assert_equal(str(c), "1.0 + 2.0·T₁(x) + 3.0·T₂(x)")
+    with pytest.raises(ValueError):
+        poly.set_default_printstyle('invalid_input')
+
+
+def test_complex_coefficients():
+    """Test both numpy and built-in complex."""
+    coefs = [0+1j, 1+1j, -2+2j, 3+0j]
+    # numpy complex
+    p1 = poly.Polynomial(coefs)
+    # Python complex
+    p2 = poly.Polynomial(array(coefs, dtype=object))
+    poly.set_default_printstyle('unicode')
+    assert_equal(str(p1), "1j + (1+1j)·x - (2-2j)·x² + (3+0j)·x³")
+    assert_equal(str(p2), "1j + (1+1j)·x + (-2+2j)·x² + (3+0j)·x³")
+    poly.set_default_printstyle('ascii')
+    assert_equal(str(p1), "1j + (1+1j) x - (2-2j) x**2 + (3+0j) x**3")
+    assert_equal(str(p2), "1j + (1+1j) x + (-2+2j) x**2 + (3+0j) x**3")
+
+
+@pytest.mark.parametrize(('coefs', 'tgt'), (
+    (array([Fraction(1, 2), Fraction(3, 4)], dtype=object), (
+        "1/2 + 3/4·x"
+    )),
+    (array([1, 2, Fraction(5, 7)], dtype=object), (
+        "1 + 2·x + 5/7·x²"
+    )),
+    (array([Decimal('1.00'), Decimal('2.2'), 3], dtype=object), (
+        "1.00 + 2.2·x + 3·x²"
+    )),
+))
+def test_numeric_object_coefficients(coefs, tgt):
+    p = poly.Polynomial(coefs)
+    poly.set_default_printstyle('unicode')
+    assert_equal(str(p), tgt)
+
+
+@pytest.mark.parametrize(('coefs', 'tgt'), (
+    (array([1, 2, 'f'], dtype=object), '1 + 2·x + f·x²'),
+    (array([1, 2, [3, 4]], dtype=object), '1 + 2·x + [3, 4]·x²'),
+))
+def test_nonnumeric_object_coefficients(coefs, tgt):
+    """
+    Test coef fallback for object arrays of non-numeric coefficients.
+    """
+    p = poly.Polynomial(coefs)
+    poly.set_default_printstyle('unicode')
+    assert_equal(str(p), tgt)
+
+
+class TestFormat:
+    def test_format_unicode(self):
+        poly.set_default_printstyle('ascii')
+        p = poly.Polynomial([1, 2, 0, -1])
+        assert_equal(format(p, 'unicode'), "1.0 + 2.0·x + 0.0·x² - 1.0·x³")
+
+    def test_format_ascii(self):
+        poly.set_default_printstyle('unicode')
+        p = poly.Polynomial([1, 2, 0, -1])
+        assert_equal(
+            format(p, 'ascii'), "1.0 + 2.0 x + 0.0 x**2 - 1.0 x**3"
+        )
+
+    def test_empty_formatstr(self):
+        poly.set_default_printstyle('ascii')
+        p = poly.Polynomial([1, 2, 3])
+        assert_equal(format(p), "1.0 + 2.0 x + 3.0 x**2")
+        assert_equal(f"{p}", "1.0 + 2.0 x + 3.0 x**2")
+
+    def test_bad_formatstr(self):
+        p = poly.Polynomial([1, 2, 0, -1])
+        with pytest.raises(ValueError):
+            format(p, '.2f')
+
+
+@pytest.mark.parametrize(('poly', 'tgt'), (
+    (poly.Polynomial, '1.0 + 2.0·z + 3.0·z²'),
+    (poly.Chebyshev, '1.0 + 2.0·T₁(z) + 3.0·T₂(z)'),
+    (poly.Hermite, '1.0 + 2.0·H₁(z) + 3.0·H₂(z)'),
+    (poly.HermiteE, '1.0 + 2.0·He₁(z) + 3.0·He₂(z)'),
+    (poly.Laguerre, '1.0 + 2.0·L₁(z) + 3.0·L₂(z)'),
+    (poly.Legendre, '1.0 + 2.0·P₁(z) + 3.0·P₂(z)'),
+))
+def test_symbol(poly, tgt):
+    p = poly([1, 2, 3], symbol='z')
+    assert_equal(f"{p:unicode}", tgt)
+
+
+class TestRepr:
+    def test_polynomial_str(self):
+        res = repr(poly.Polynomial([0, 1]))
+        tgt = (
+            "Polynomial([0., 1.], domain=[-1,  1], window=[-1,  1], "
+            "symbol='x')"
+        )
+        assert_equal(res, tgt)
+
+    def test_chebyshev_str(self):
+        res = repr(poly.Chebyshev([0, 1]))
+        tgt = (
+            "Chebyshev([0., 1.], domain=[-1,  1], window=[-1,  1], "
+            "symbol='x')"
+        )
+        assert_equal(res, tgt)
+
+    def test_legendre_repr(self):
+        res = repr(poly.Legendre([0, 1]))
+        tgt = (
+            "Legendre([0., 1.], domain=[-1,  1], window=[-1,  1], "
+            "symbol='x')"
+        )
+        assert_equal(res, tgt)
+
+    def test_hermite_repr(self):
+        res = repr(poly.Hermite([0, 1]))
+        tgt = (
+            "Hermite([0., 1.], domain=[-1,  1], window=[-1,  1], "
+            "symbol='x')"
+        )
+        assert_equal(res, tgt)
+
+    def test_hermiteE_repr(self):
+        res = repr(poly.HermiteE([0, 1]))
+        tgt = (
+            "HermiteE([0., 1.], domain=[-1,  1], window=[-1,  1], "
+            "symbol='x')"
+        )
+        assert_equal(res, tgt)
+
+    def test_laguerre_repr(self):
+        res = repr(poly.Laguerre([0, 1]))
+        tgt = (
+            "Laguerre([0., 1.], domain=[0, 1], window=[0, 1], "
+            "symbol='x')"
+        )
+        assert_equal(res, tgt)
+
+
+class TestLatexRepr:
+    """Test the latex repr used by Jupyter"""
+
+    def as_latex(self, obj):
+        # right now we ignore the formatting of scalars in our tests, since
+        # it makes them too verbose. Ideally, the formatting of scalars will
+        # be fixed such that tests below continue to pass
+        obj._repr_latex_scalar = lambda x, parens=False: str(x)
+        try:
+            return obj._repr_latex_()
+        finally:
+            del obj._repr_latex_scalar
+
+    def test_simple_polynomial(self):
+        # default input
+        p = poly.Polynomial([1, 2, 3])
+        assert_equal(self.as_latex(p),
+            r'$x \mapsto 1.0 + 2.0\,x + 3.0\,x^{2}$')
+
+        # translated input
+        p = poly.Polynomial([1, 2, 3], domain=[-2, 0])
+        assert_equal(self.as_latex(p),
+            r'$x \mapsto 1.0 + 2.0\,\left(1.0 + x\right) + 3.0\,\left(1.0 + x\right)^{2}$')
+
+        # scaled input
+        p = poly.Polynomial([1, 2, 3], domain=[-0.5, 0.5])
+        assert_equal(self.as_latex(p),
+            r'$x \mapsto 1.0 + 2.0\,\left(2.0x\right) + 3.0\,\left(2.0x\right)^{2}$')
+
+        # affine input
+        p = poly.Polynomial([1, 2, 3], domain=[-1, 0])
+        assert_equal(self.as_latex(p),
+            r'$x \mapsto 1.0 + 2.0\,\left(1.0 + 2.0x\right) + 3.0\,\left(1.0 + 2.0x\right)^{2}$')
+
+    def test_basis_func(self):
+        p = poly.Chebyshev([1, 2, 3])
+        assert_equal(self.as_latex(p),
+            r'$x \mapsto 1.0\,{T}_{0}(x) + 2.0\,{T}_{1}(x) + 3.0\,{T}_{2}(x)$')
+        # affine input - check no surplus parens are added
+        p = poly.Chebyshev([1, 2, 3], domain=[-1, 0])
+        assert_equal(self.as_latex(p),
+            r'$x \mapsto 1.0\,{T}_{0}(1.0 + 2.0x) + 2.0\,{T}_{1}(1.0 + 2.0x) + 3.0\,{T}_{2}(1.0 + 2.0x)$')
+
+    def test_multichar_basis_func(self):
+        p = poly.HermiteE([1, 2, 3])
+        assert_equal(self.as_latex(p),
+            r'$x \mapsto 1.0\,{He}_{0}(x) + 2.0\,{He}_{1}(x) + 3.0\,{He}_{2}(x)$')
+
+    def test_symbol_basic(self):
+        # default input
+        p = poly.Polynomial([1, 2, 3], symbol='z')
+        assert_equal(self.as_latex(p),
+            r'$z \mapsto 1.0 + 2.0\,z + 3.0\,z^{2}$')
+
+        # translated input
+        p = poly.Polynomial([1, 2, 3], domain=[-2, 0], symbol='z')
+        assert_equal(
+            self.as_latex(p),
+            (
+                r'$z \mapsto 1.0 + 2.0\,\left(1.0 + z\right) + 3.0\,'
+                r'\left(1.0 + z\right)^{2}$'
+            ),
+        )
+
+        # scaled input
+        p = poly.Polynomial([1, 2, 3], domain=[-0.5, 0.5], symbol='z')
+        assert_equal(
+            self.as_latex(p),
+            (
+                r'$z \mapsto 1.0 + 2.0\,\left(2.0z\right) + 3.0\,'
+                r'\left(2.0z\right)^{2}$'
+            ),
+        )
+
+        # affine input
+        p = poly.Polynomial([1, 2, 3], domain=[-1, 0], symbol='z')
+        assert_equal(
+            self.as_latex(p),
+            (
+                r'$z \mapsto 1.0 + 2.0\,\left(1.0 + 2.0z\right) + 3.0\,'
+                r'\left(1.0 + 2.0z\right)^{2}$'
+            ),
+        )
+
+
+SWITCH_TO_EXP = (
+    '1.0 + (1.0e-01) x + (1.0e-02) x**2',
+    '1.2 + (1.2e-01) x + (1.2e-02) x**2',
+    '1.23 + 0.12 x + (1.23e-02) x**2 + (1.23e-03) x**3',
+    '1.235 + 0.123 x + (1.235e-02) x**2 + (1.235e-03) x**3',
+    '1.2346 + 0.1235 x + 0.0123 x**2 + (1.2346e-03) x**3 + (1.2346e-04) x**4',
+    '1.23457 + 0.12346 x + 0.01235 x**2 + (1.23457e-03) x**3 + '
+    '(1.23457e-04) x**4',
+    '1.234568 + 0.123457 x + 0.012346 x**2 + 0.001235 x**3 + '
+    '(1.234568e-04) x**4 + (1.234568e-05) x**5',
+    '1.2345679 + 0.1234568 x + 0.0123457 x**2 + 0.0012346 x**3 + '
+    '(1.2345679e-04) x**4 + (1.2345679e-05) x**5')
+
+class TestPrintOptions:
+    """
+    Test the output is properly configured via printoptions.
+    The exponential notation is enabled automatically when the values 
+    are too small or too large.
+    """
+
+    @pytest.fixture(scope='class', autouse=True)
+    def use_ascii(self):
+        poly.set_default_printstyle('ascii')
+
+    def test_str(self):
+        p = poly.Polynomial([1/2, 1/7, 1/7*10**8, 1/7*10**9])
+        assert_equal(str(p), '0.5 + 0.14285714 x + 14285714.28571429 x**2 '
+                             '+ (1.42857143e+08) x**3')
+
+        with printoptions(precision=3):
+            assert_equal(str(p), '0.5 + 0.143 x + 14285714.286 x**2 '
+                                 '+ (1.429e+08) x**3')
+
+    def test_latex(self):
+        p = poly.Polynomial([1/2, 1/7, 1/7*10**8, 1/7*10**9])
+        assert_equal(p._repr_latex_(),
+            r'$x \mapsto \text{0.5} + \text{0.14285714}\,x + '
+            r'\text{14285714.28571429}\,x^{2} + '
+            r'\text{(1.42857143e+08)}\,x^{3}$')
+        
+        with printoptions(precision=3):
+            assert_equal(p._repr_latex_(),
+                r'$x \mapsto \text{0.5} + \text{0.143}\,x + '
+                r'\text{14285714.286}\,x^{2} + \text{(1.429e+08)}\,x^{3}$')
+
+    def test_fixed(self):
+        p = poly.Polynomial([1/2])
+        assert_equal(str(p), '0.5')
+        
+        with printoptions(floatmode='fixed'):
+            assert_equal(str(p), '0.50000000')
+        
+        with printoptions(floatmode='fixed', precision=4):
+            assert_equal(str(p), '0.5000')
+
+    def test_switch_to_exp(self):
+        for i, s in enumerate(SWITCH_TO_EXP):
+            with printoptions(precision=i):
+                p = poly.Polynomial([1.23456789*10**-i 
+                                     for i in range(i//2+3)])
+                assert str(p).replace('\n', ' ') == s 
+    
+    def test_non_finite(self):
+        p = poly.Polynomial([nan, inf])
+        assert str(p) == 'nan + inf x'
+        assert p._repr_latex_() == r'$x \mapsto \text{nan} + \text{inf}\,x$'
+        with printoptions(nanstr='NAN', infstr='INF'):
+            assert str(p) == 'NAN + INF x'
+            assert p._repr_latex_() == \
+                r'$x \mapsto \text{NAN} + \text{INF}\,x$'
diff --git a/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_symbol.py b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_symbol.py
new file mode 100644
index 00000000..4ea6035e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/numpy/polynomial/tests/test_symbol.py
@@ -0,0 +1,216 @@
+"""
+Tests related to the ``symbol`` attribute of the ABCPolyBase class.
+"""
+
+import pytest
+import numpy.polynomial as poly
+from numpy.core import array
+from numpy.testing import assert_equal, assert_raises, assert_
+
+
+class TestInit:
+    """
+    Test polynomial creation with symbol kwarg.
+    """
+    c = [1, 2, 3]
+
+    def test_default_symbol(self):
+        p = poly.Polynomial(self.c)
+        assert_equal(p.symbol, 'x')
+
+    @pytest.mark.parametrize(('bad_input', 'exception'), (
+        ('', ValueError),
+        ('3', ValueError),
+        (None, TypeError),
+        (1, TypeError),
+    ))
+    def test_symbol_bad_input(self, bad_input, exception):
+        with pytest.raises(exception):
+            p = poly.Polynomial(self.c, symbol=bad_input)
+
+    @pytest.mark.parametrize('symbol', (
+        'x',
+        'x_1',
+        'A',
+        'xyz',
+        'β',
+    ))
+    def test_valid_symbols(self, symbol):
+        """
+        Values for symbol that should pass input validation.
+        """
+        p = poly.Polynomial(self.c, symbol=symbol)
+        assert_equal(p.symbol, symbol)
+
+    def test_property(self):
+        """
+        'symbol' attribute is read only.
+        """
+        p = poly.Polynomial(self.c, symbol='x')
+        with pytest.raises(AttributeError):
+            p.symbol = 'z'
+
+    def test_change_symbol(self):
+        p = poly.Polynomial(self.c, symbol='y')
+        # Create new polynomial from p with different symbol
+        pt = poly.Polynomial(p.coef, symbol='t')
+        assert_equal(pt.symbol, 't')
+
+
+class TestUnaryOperators:
+    p = poly.Polynomial([1, 2, 3], symbol='z')
+
+    def test_neg(self):
+        n = -self.p
+        assert_equal(n.symbol, 'z')
+
+    def test_scalarmul(self):
+        out = self.p * 10
+        assert_equal(out.symbol, 'z')
+
+    def test_rscalarmul(self):
+        out = 10 * self.p
+        assert_equal(out.symbol, 'z')
+
+    def test_pow(self):
+        out = self.p ** 3
+        assert_equal(out.symbol, 'z')
+
+
+@pytest.mark.parametrize(
+    'rhs',
+    (
+        poly.Polynomial([4, 5, 6], symbol='z'),
+        array([4, 5, 6]),
+    ),
+)
+class TestBinaryOperatorsSameSymbol:
+    """
+    Ensure symbol is preserved for numeric operations on polynomials with
+    the same symbol
+    """
+    p = poly.Polynomial([1, 2, 3], symbol='z')
+
+    def test_add(self, rhs):
+        out = self.p + rhs
+        assert_equal(out.symbol, 'z')
+
+    def test_sub(self, rhs):
+        out = self.p - rhs
+        assert_equal(out.symbol, 'z')
+
+    def test_polymul(self, rhs):
+        out = self.p * rhs
+        assert_equal(out.symbol, 'z')
+
+    def test_divmod(self, rhs):
+        for out in divmod(self.p, rhs):
+            assert_equal(out.symbol, 'z')
+
+    def test_radd(self, rhs):
+        out = rhs + self.p
+        assert_equal(out.symbol, 'z')
+
+    def test_rsub(self, rhs):
+        out = rhs - self.p
+        assert_equal(out.symbol, 'z')
+
+    def test_rmul(self, rhs):
+        out = rhs * self.p
+        assert_equal(out.symbol, 'z')
+
+    def test_rdivmod(self, rhs):
+        for out in divmod(rhs, self.p):
+            assert_equal(out.symbol, 'z')
+
+
+class TestBinaryOperatorsDifferentSymbol:
+    p = poly.Polynomial([1, 2, 3], symbol='x')
+    other = poly.Polynomial([4, 5, 6], symbol='y')
+    ops = (p.__add__, p.__sub__, p.__mul__, p.__floordiv__, p.__mod__)
+
+    @pytest.mark.parametrize('f', ops)
+    def test_binops_fails(self, f):
+        assert_raises(ValueError, f, self.other)
+
+
+class TestEquality:
+    p = poly.Polynomial([1, 2, 3], symbol='x')
+
+    def test_eq(self):
+        other = poly.Polynomial([1, 2, 3], symbol='x')
+        assert_(self.p == other)
+
+    def test_neq(self):
+        other = poly.Polynomial([1, 2, 3], symbol='y')
+        assert_(not self.p == other)
+
+
+class TestExtraMethods:
+    """
+    Test other methods for manipulating/creating polynomial objects.
+    """
+    p = poly.Polynomial([1, 2, 3, 0], symbol='z')
+
+    def test_copy(self):
+        other = self.p.copy()
+        assert_equal(other.symbol, 'z')
+
+    def test_trim(self):
+        other = self.p.trim()
+        assert_equal(other.symbol, 'z')
+
+    def test_truncate(self):
+        other = self.p.truncate(2)
+        assert_equal(other.symbol, 'z')
+
+    @pytest.mark.parametrize('kwarg', (
+        {'domain': [-10, 10]},
+        {'window': [-10, 10]},
+        {'kind': poly.Chebyshev},
+    ))
+    def test_convert(self, kwarg):
+        other = self.p.convert(**kwarg)
+        assert_equal(other.symbol, 'z')
+
+    def test_integ(self):
+        other = self.p.integ()
+        assert_equal(other.symbol, 'z')
+
+    def test_deriv(self):
+        other = self.p.deriv()
+        assert_equal(other.symbol, 'z')
+
+
+def test_composition():
+    p = poly.Polynomial([3, 2, 1], symbol="t")
+    q = poly.Polynomial([5, 1, 0, -1], symbol="λ_1")
+    r = p(q)
+    assert r.symbol == "λ_1"
+
+
+#
+# Class methods that result in new polynomial class instances
+#
+
+
+def test_fit():
+    x, y = (range(10),)*2
+    p = poly.Polynomial.fit(x, y, deg=1, symbol='z')
+    assert_equal(p.symbol, 'z')
+
+
+def test_froomroots():
+    roots = [-2, 2]
+    p = poly.Polynomial.fromroots(roots, symbol='z')
+    assert_equal(p.symbol, 'z')
+
+
+def test_identity():
+    p = poly.Polynomial.identity(domain=[-1, 1], window=[5, 20], symbol='z')
+    assert_equal(p.symbol, 'z')
+
+
+def test_basis():
+    p = poly.Polynomial.basis(3, symbol='z')
+    assert_equal(p.symbol, 'z')