diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__init__.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2140970015d099763797d60a98a3a3b6f4b5f219 --- /dev/null +++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__init__.py @@ -0,0 +1,14 @@ +""" +Module containing private utility functions +=========================================== + +The ``scipy._lib`` namespace is empty (for now). Tests for all +utilities in submodules of ``_lib`` can be run with:: + + from scipy import _lib + _lib.test() + +""" +from scipy._lib._testutils import PytestTester +test = PytestTester(__name__) +del PytestTester diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/__init__.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7971b209e7ab27401cc3b430c25cad77f5d2c6d Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/__init__.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_array_api.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_array_api.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e2217464a5e007691d154a228b4da25bd3ab993 Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_array_api.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_bunch.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_bunch.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf89e52e0777f526af74deabf1da45f2741effee Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_bunch.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_ccallback.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_ccallback.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dada1c06a74bf9fac597506c0fa6405cbd1a29c4 Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_ccallback.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_disjoint_set.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_disjoint_set.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c8a464b97890e01ef467146dc12798cdebf7627 Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_disjoint_set.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_docscrape.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_docscrape.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e931b2cc7aefb32591aed3e39765a785fe15608 Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_docscrape.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_elementwise_iterative_method.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_elementwise_iterative_method.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dad426cce8fd37a38f5b61757c213f639a0e56cf Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_elementwise_iterative_method.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_finite_differences.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_finite_differences.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2fb817703218d12939e19ce48ddf1717bd3185fc Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_finite_differences.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_pep440.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_pep440.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9fb259070e5fc54438af9eee9779853a5314fe8 Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_pep440.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_testutils.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_testutils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c0d8406153f6c7f0665334413beba8f6c20dd5ad Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_testutils.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_threadsafety.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_threadsafety.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..11cec7fddffd68d4dc13ffe35351dcd43529550f Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_threadsafety.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_tmpdirs.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_tmpdirs.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc40e92b6cf59011a58355006c4c4657173c073a Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_tmpdirs.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_util.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_util.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34cb0aa2227ba75bb2aef053c1f7e48c7b41467d Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/_util.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/decorator.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/decorator.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5b1fba476792395e25e1f14124cf48fe688852f Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/decorator.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/deprecation.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/deprecation.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2c2463d156b99752c43007af5bffd0d9ce686cb Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/__pycache__/deprecation.cpython-310.pyc differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_array_api.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_array_api.py new file mode 100644 index 0000000000000000000000000000000000000000..5cce1726013fef80c4a95998d8ab41cb026e5cdf --- /dev/null +++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_array_api.py @@ -0,0 +1,524 @@ +"""Utility functions to use Python Array API compatible libraries. + +For the context about the Array API see: +https://data-apis.org/array-api/latest/purpose_and_scope.html + +The SciPy use case of the Array API is described on the following page: +https://data-apis.org/array-api/latest/use_cases.html#use-case-scipy +""" +from __future__ import annotations + +import os +import warnings + +from types import ModuleType +from typing import Any, Literal, TYPE_CHECKING + +import numpy as np +import numpy.typing as npt + +from scipy._lib import array_api_compat +from scipy._lib.array_api_compat import ( + is_array_api_obj, + size, + numpy as np_compat, + device +) + +__all__ = ['array_namespace', '_asarray', 'size', 'device'] + + +# To enable array API and strict array-like input validation +SCIPY_ARRAY_API: str | bool = os.environ.get("SCIPY_ARRAY_API", False) +# To control the default device - for use in the test suite only +SCIPY_DEVICE = os.environ.get("SCIPY_DEVICE", "cpu") + +_GLOBAL_CONFIG = { + "SCIPY_ARRAY_API": SCIPY_ARRAY_API, + "SCIPY_DEVICE": SCIPY_DEVICE, +} + + +if TYPE_CHECKING: + Array = Any # To be changed to a Protocol later (see array-api#589) + ArrayLike = Array | npt.ArrayLike + + +def compliance_scipy(arrays: list[ArrayLike]) -> list[Array]: + """Raise exceptions on known-bad subclasses. + + The following subclasses are not supported and raise and error: + - `numpy.ma.MaskedArray` + - `numpy.matrix` + - NumPy arrays which do not have a boolean or numerical dtype + - Any array-like which is neither array API compatible nor coercible by NumPy + - Any array-like which is coerced by NumPy to an unsupported dtype + """ + for i in range(len(arrays)): + array = arrays[i] + if isinstance(array, np.ma.MaskedArray): + raise TypeError("Inputs of type `numpy.ma.MaskedArray` are not supported.") + elif isinstance(array, np.matrix): + raise TypeError("Inputs of type `numpy.matrix` are not supported.") + if isinstance(array, (np.ndarray, np.generic)): + dtype = array.dtype + if not (np.issubdtype(dtype, np.number) or np.issubdtype(dtype, np.bool_)): + raise TypeError(f"An argument has dtype `{dtype!r}`; " + f"only boolean and numerical dtypes are supported.") + elif not is_array_api_obj(array): + try: + array = np.asanyarray(array) + except TypeError: + raise TypeError("An argument is neither array API compatible nor " + "coercible by NumPy.") + dtype = array.dtype + if not (np.issubdtype(dtype, np.number) or np.issubdtype(dtype, np.bool_)): + message = ( + f"An argument was coerced to an unsupported dtype `{dtype!r}`; " + f"only boolean and numerical dtypes are supported." + ) + raise TypeError(message) + arrays[i] = array + return arrays + + +def _check_finite(array: Array, xp: ModuleType) -> None: + """Check for NaNs or Infs.""" + msg = "array must not contain infs or NaNs" + try: + if not xp.all(xp.isfinite(array)): + raise ValueError(msg) + except TypeError: + raise ValueError(msg) + + +def array_namespace(*arrays: Array) -> ModuleType: + """Get the array API compatible namespace for the arrays xs. + + Parameters + ---------- + *arrays : sequence of array_like + Arrays used to infer the common namespace. + + Returns + ------- + namespace : module + Common namespace. + + Notes + ----- + Thin wrapper around `array_api_compat.array_namespace`. + + 1. Check for the global switch: SCIPY_ARRAY_API. This can also be accessed + dynamically through ``_GLOBAL_CONFIG['SCIPY_ARRAY_API']``. + 2. `compliance_scipy` raise exceptions on known-bad subclasses. See + its definition for more details. + + When the global switch is False, it defaults to the `numpy` namespace. + In that case, there is no compliance check. This is a convenience to + ease the adoption. Otherwise, arrays must comply with the new rules. + """ + if not _GLOBAL_CONFIG["SCIPY_ARRAY_API"]: + # here we could wrap the namespace if needed + return np_compat + + _arrays = [array for array in arrays if array is not None] + + _arrays = compliance_scipy(_arrays) + + return array_api_compat.array_namespace(*_arrays) + + +def _asarray( + array: ArrayLike, + dtype: Any = None, + order: Literal['K', 'A', 'C', 'F'] | None = None, + copy: bool | None = None, + *, + xp: ModuleType | None = None, + check_finite: bool = False, + subok: bool = False, + ) -> Array: + """SciPy-specific replacement for `np.asarray` with `order`, `check_finite`, and + `subok`. + + Memory layout parameter `order` is not exposed in the Array API standard. + `order` is only enforced if the input array implementation + is NumPy based, otherwise `order` is just silently ignored. + + `check_finite` is also not a keyword in the array API standard; included + here for convenience rather than that having to be a separate function + call inside SciPy functions. + + `subok` is included to allow this function to preserve the behaviour of + `np.asanyarray` for NumPy based inputs. + """ + if xp is None: + xp = array_namespace(array) + if xp.__name__ in {"numpy", "scipy._lib.array_api_compat.numpy"}: + # Use NumPy API to support order + if copy is True: + array = np.array(array, order=order, dtype=dtype, subok=subok) + elif subok: + array = np.asanyarray(array, order=order, dtype=dtype) + else: + array = np.asarray(array, order=order, dtype=dtype) + + # At this point array is a NumPy ndarray. We convert it to an array + # container that is consistent with the input's namespace. + array = xp.asarray(array) + else: + try: + array = xp.asarray(array, dtype=dtype, copy=copy) + except TypeError: + coerced_xp = array_namespace(xp.asarray(3)) + array = coerced_xp.asarray(array, dtype=dtype, copy=copy) + + if check_finite: + _check_finite(array, xp) + + return array + + +def atleast_nd(x: Array, *, ndim: int, xp: ModuleType | None = None) -> Array: + """Recursively expand the dimension to have at least `ndim`.""" + if xp is None: + xp = array_namespace(x) + x = xp.asarray(x) + if x.ndim < ndim: + x = xp.expand_dims(x, axis=0) + x = atleast_nd(x, ndim=ndim, xp=xp) + return x + + +def copy(x: Array, *, xp: ModuleType | None = None) -> Array: + """ + Copies an array. + + Parameters + ---------- + x : array + + xp : array_namespace + + Returns + ------- + copy : array + Copied array + + Notes + ----- + This copy function does not offer all the semantics of `np.copy`, i.e. the + `subok` and `order` keywords are not used. + """ + # Note: xp.asarray fails if xp is numpy. + if xp is None: + xp = array_namespace(x) + + return _asarray(x, copy=True, xp=xp) + + +def is_numpy(xp: ModuleType) -> bool: + return xp.__name__ in ('numpy', 'scipy._lib.array_api_compat.numpy') + + +def is_cupy(xp: ModuleType) -> bool: + return xp.__name__ in ('cupy', 'scipy._lib.array_api_compat.cupy') + + +def is_torch(xp: ModuleType) -> bool: + return xp.__name__ in ('torch', 'scipy._lib.array_api_compat.torch') + +def is_jax(xp): + return xp.__name__ in ('jax.numpy', 'jax.experimental.array_api') + + +def _strict_check(actual, desired, xp, + check_namespace=True, check_dtype=True, check_shape=True): + __tracebackhide__ = True # Hide traceback for py.test + if check_namespace: + _assert_matching_namespace(actual, desired) + + desired = xp.asarray(desired) + + if check_dtype: + _msg = f"dtypes do not match.\nActual: {actual.dtype}\nDesired: {desired.dtype}" + assert actual.dtype == desired.dtype, _msg + + if check_shape: + _msg = f"Shapes do not match.\nActual: {actual.shape}\nDesired: {desired.shape}" + assert actual.shape == desired.shape, _msg + _check_scalar(actual, desired, xp) + + desired = xp.broadcast_to(desired, actual.shape) + return desired + + +def _assert_matching_namespace(actual, desired): + __tracebackhide__ = True # Hide traceback for py.test + actual = actual if isinstance(actual, tuple) else (actual,) + desired_space = array_namespace(desired) + for arr in actual: + arr_space = array_namespace(arr) + _msg = (f"Namespaces do not match.\n" + f"Actual: {arr_space.__name__}\n" + f"Desired: {desired_space.__name__}") + assert arr_space == desired_space, _msg + + +def _check_scalar(actual, desired, xp): + __tracebackhide__ = True # Hide traceback for py.test + # Shape check alone is sufficient unless desired.shape == (). Also, + # only NumPy distinguishes between scalars and arrays. + if desired.shape != () or not is_numpy(xp): + return + # We want to follow the conventions of the `xp` library. Libraries like + # NumPy, for which `np.asarray(0)[()]` returns a scalar, tend to return + # a scalar even when a 0D array might be more appropriate: + # import numpy as np + # np.mean([1, 2, 3]) # scalar, not 0d array + # np.asarray(0)*2 # scalar, not 0d array + # np.sin(np.asarray(0)) # scalar, not 0d array + # Libraries like CuPy, for which `cp.asarray(0)[()]` returns a 0D array, + # tend to return a 0D array in scenarios like those above. + # Therefore, regardless of whether the developer provides a scalar or 0D + # array for `desired`, we would typically want the type of `actual` to be + # the type of `desired[()]`. If the developer wants to override this + # behavior, they can set `check_shape=False`. + desired = desired[()] + _msg = f"Types do not match:\n Actual: {type(actual)}\n Desired: {type(desired)}" + assert (xp.isscalar(actual) and xp.isscalar(desired) + or (not xp.isscalar(actual) and not xp.isscalar(desired))), _msg + + +def xp_assert_equal(actual, desired, check_namespace=True, check_dtype=True, + check_shape=True, err_msg='', xp=None): + __tracebackhide__ = True # Hide traceback for py.test + if xp is None: + xp = array_namespace(actual) + desired = _strict_check(actual, desired, xp, check_namespace=check_namespace, + check_dtype=check_dtype, check_shape=check_shape) + if is_cupy(xp): + return xp.testing.assert_array_equal(actual, desired, err_msg=err_msg) + elif is_torch(xp): + # PyTorch recommends using `rtol=0, atol=0` like this + # to test for exact equality + err_msg = None if err_msg == '' else err_msg + return xp.testing.assert_close(actual, desired, rtol=0, atol=0, equal_nan=True, + check_dtype=False, msg=err_msg) + # JAX uses `np.testing` + return np.testing.assert_array_equal(actual, desired, err_msg=err_msg) + + +def xp_assert_close(actual, desired, rtol=None, atol=0, check_namespace=True, + check_dtype=True, check_shape=True, err_msg='', xp=None): + __tracebackhide__ = True # Hide traceback for py.test + if xp is None: + xp = array_namespace(actual) + desired = _strict_check(actual, desired, xp, check_namespace=check_namespace, + check_dtype=check_dtype, check_shape=check_shape) + + floating = xp.isdtype(actual.dtype, ('real floating', 'complex floating')) + if rtol is None and floating: + # multiplier of 4 is used as for `np.float64` this puts the default `rtol` + # roughly half way between sqrt(eps) and the default for + # `numpy.testing.assert_allclose`, 1e-7 + rtol = xp.finfo(actual.dtype).eps**0.5 * 4 + elif rtol is None: + rtol = 1e-7 + + if is_cupy(xp): + return xp.testing.assert_allclose(actual, desired, rtol=rtol, + atol=atol, err_msg=err_msg) + elif is_torch(xp): + err_msg = None if err_msg == '' else err_msg + return xp.testing.assert_close(actual, desired, rtol=rtol, atol=atol, + equal_nan=True, check_dtype=False, msg=err_msg) + # JAX uses `np.testing` + return np.testing.assert_allclose(actual, desired, rtol=rtol, + atol=atol, err_msg=err_msg) + + +def xp_assert_less(actual, desired, check_namespace=True, check_dtype=True, + check_shape=True, err_msg='', verbose=True, xp=None): + __tracebackhide__ = True # Hide traceback for py.test + if xp is None: + xp = array_namespace(actual) + desired = _strict_check(actual, desired, xp, check_namespace=check_namespace, + check_dtype=check_dtype, check_shape=check_shape) + if is_cupy(xp): + return xp.testing.assert_array_less(actual, desired, + err_msg=err_msg, verbose=verbose) + elif is_torch(xp): + if actual.device.type != 'cpu': + actual = actual.cpu() + if desired.device.type != 'cpu': + desired = desired.cpu() + # JAX uses `np.testing` + return np.testing.assert_array_less(actual, desired, + err_msg=err_msg, verbose=verbose) + + +def cov(x: Array, *, xp: ModuleType | None = None) -> Array: + if xp is None: + xp = array_namespace(x) + + X = copy(x, xp=xp) + dtype = xp.result_type(X, xp.float64) + + X = atleast_nd(X, ndim=2, xp=xp) + X = xp.asarray(X, dtype=dtype) + + avg = xp.mean(X, axis=1) + fact = X.shape[1] - 1 + + if fact <= 0: + warnings.warn("Degrees of freedom <= 0 for slice", + RuntimeWarning, stacklevel=2) + fact = 0.0 + + X -= avg[:, None] + X_T = X.T + if xp.isdtype(X_T.dtype, 'complex floating'): + X_T = xp.conj(X_T) + c = X @ X_T + c /= fact + axes = tuple(axis for axis, length in enumerate(c.shape) if length == 1) + return xp.squeeze(c, axis=axes) + + +def xp_unsupported_param_msg(param: Any) -> str: + return f'Providing {param!r} is only supported for numpy arrays.' + + +def is_complex(x: Array, xp: ModuleType) -> bool: + return xp.isdtype(x.dtype, 'complex floating') + + +def get_xp_devices(xp: ModuleType) -> list[str] | list[None]: + """Returns a list of available devices for the given namespace.""" + devices: list[str] = [] + if is_torch(xp): + devices += ['cpu'] + import torch # type: ignore[import] + num_cuda = torch.cuda.device_count() + for i in range(0, num_cuda): + devices += [f'cuda:{i}'] + if torch.backends.mps.is_available(): + devices += ['mps'] + return devices + elif is_cupy(xp): + import cupy # type: ignore[import] + num_cuda = cupy.cuda.runtime.getDeviceCount() + for i in range(0, num_cuda): + devices += [f'cuda:{i}'] + return devices + elif is_jax(xp): + import jax # type: ignore[import] + num_cpu = jax.device_count(backend='cpu') + for i in range(0, num_cpu): + devices += [f'cpu:{i}'] + num_gpu = jax.device_count(backend='gpu') + for i in range(0, num_gpu): + devices += [f'gpu:{i}'] + num_tpu = jax.device_count(backend='tpu') + for i in range(0, num_tpu): + devices += [f'tpu:{i}'] + return devices + + # given namespace is not known to have a list of available devices; + # return `[None]` so that one can use this in tests for `device=None`. + return [None] + + +def scipy_namespace_for(xp: ModuleType) -> ModuleType: + """ + Return the `scipy` namespace for alternative backends, where it exists, + such as `cupyx.scipy` and `jax.scipy`. Useful for ad hoc dispatching. + + Default: return `scipy` (this package). + """ + + + if is_cupy(xp): + import cupyx # type: ignore[import-not-found,import-untyped] + return cupyx.scipy + + if is_jax(xp): + import jax # type: ignore[import-not-found] + return jax.scipy + + import scipy + return scipy + + +# temporary substitute for xp.minimum, which is not yet in all backends +# or covered by array_api_compat. +def xp_minimum(x1: Array, x2: Array, /) -> Array: + # xp won't be passed in because it doesn't need to be passed in to xp.minimum + xp = array_namespace(x1, x2) + if hasattr(xp, 'minimum'): + return xp.minimum(x1, x2) + x1, x2 = xp.broadcast_arrays(x1, x2) + i = (x2 < x1) | xp.isnan(x2) + res = xp.where(i, x2, x1) + return res[()] if res.ndim == 0 else res + + +# temporary substitute for xp.clip, which is not yet in all backends +# or covered by array_api_compat. +def xp_clip( + x: Array, + /, + min: int | float | Array | None = None, + max: int | float | Array | None = None, + *, + xp: ModuleType | None = None) -> Array: + xp = array_namespace(x) if xp is None else xp + a, b = xp.asarray(min, dtype=x.dtype), xp.asarray(max, dtype=x.dtype) + if hasattr(xp, 'clip'): + return xp.clip(x, a, b) + x, a, b = xp.broadcast_arrays(x, a, b) + y = xp.asarray(x, copy=True) + ia = y < a + y[ia] = a[ia] + ib = y > b + y[ib] = b[ib] + return y[()] if y.ndim == 0 else y + + +# temporary substitute for xp.moveaxis, which is not yet in all backends +# or covered by array_api_compat. +def xp_moveaxis_to_end( + x: Array, + source: int, + /, *, + xp: ModuleType | None = None) -> Array: + xp = array_namespace(xp) if xp is None else xp + axes = list(range(x.ndim)) + temp = axes.pop(source) + axes = axes + [temp] + return xp.permute_dims(x, axes) + + +# temporary substitute for xp.copysign, which is not yet in all backends +# or covered by array_api_compat. +def xp_copysign(x1: Array, x2: Array, /, *, xp: ModuleType | None = None) -> Array: + # no attempt to account for special cases + xp = array_namespace(x1, x2) if xp is None else xp + abs_x1 = xp.abs(x1) + return xp.where(x2 >= 0, abs_x1, -abs_x1) + + +# partial substitute for xp.sign, which does not cover the NaN special case +# that I need. (https://github.com/data-apis/array-api-compat/issues/136) +def xp_sign(x: Array, /, *, xp: ModuleType | None = None) -> Array: + xp = array_namespace(x) if xp is None else xp + if is_numpy(xp): # only NumPy implements the special cases correctly + return xp.sign(x) + sign = xp.full_like(x, xp.nan) + one = xp.asarray(1, dtype=x.dtype) + sign = xp.where(x > 0, one, sign) + sign = xp.where(x < 0, -one, sign) + sign = xp.where(x == 0, 0*one, sign) + return sign diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_bunch.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_bunch.py new file mode 100644 index 0000000000000000000000000000000000000000..bb562e4348f46dc1137afe3d3ce50f1149c85376 --- /dev/null +++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_bunch.py @@ -0,0 +1,225 @@ +import sys as _sys +from keyword import iskeyword as _iskeyword + + +def _validate_names(typename, field_names, extra_field_names): + """ + Ensure that all the given names are valid Python identifiers that + do not start with '_'. Also check that there are no duplicates + among field_names + extra_field_names. + """ + for name in [typename] + field_names + extra_field_names: + if not isinstance(name, str): + raise TypeError('typename and all field names must be strings') + if not name.isidentifier(): + raise ValueError('typename and all field names must be valid ' + f'identifiers: {name!r}') + if _iskeyword(name): + raise ValueError('typename and all field names cannot be a ' + f'keyword: {name!r}') + + seen = set() + for name in field_names + extra_field_names: + if name.startswith('_'): + raise ValueError('Field names cannot start with an underscore: ' + f'{name!r}') + if name in seen: + raise ValueError(f'Duplicate field name: {name!r}') + seen.add(name) + + +# Note: This code is adapted from CPython:Lib/collections/__init__.py +def _make_tuple_bunch(typename, field_names, extra_field_names=None, + module=None): + """ + Create a namedtuple-like class with additional attributes. + + This function creates a subclass of tuple that acts like a namedtuple + and that has additional attributes. + + The additional attributes are listed in `extra_field_names`. The + values assigned to these attributes are not part of the tuple. + + The reason this function exists is to allow functions in SciPy + that currently return a tuple or a namedtuple to returned objects + that have additional attributes, while maintaining backwards + compatibility. + + This should only be used to enhance *existing* functions in SciPy. + New functions are free to create objects as return values without + having to maintain backwards compatibility with an old tuple or + namedtuple return value. + + Parameters + ---------- + typename : str + The name of the type. + field_names : list of str + List of names of the values to be stored in the tuple. These names + will also be attributes of instances, so the values in the tuple + can be accessed by indexing or as attributes. At least one name + is required. See the Notes for additional restrictions. + extra_field_names : list of str, optional + List of names of values that will be stored as attributes of the + object. See the notes for additional restrictions. + + Returns + ------- + cls : type + The new class. + + Notes + ----- + There are restrictions on the names that may be used in `field_names` + and `extra_field_names`: + + * The names must be unique--no duplicates allowed. + * The names must be valid Python identifiers, and must not begin with + an underscore. + * The names must not be Python keywords (e.g. 'def', 'and', etc., are + not allowed). + + Examples + -------- + >>> from scipy._lib._bunch import _make_tuple_bunch + + Create a class that acts like a namedtuple with length 2 (with field + names `x` and `y`) that will also have the attributes `w` and `beta`: + + >>> Result = _make_tuple_bunch('Result', ['x', 'y'], ['w', 'beta']) + + `Result` is the new class. We call it with keyword arguments to create + a new instance with given values. + + >>> result1 = Result(x=1, y=2, w=99, beta=0.5) + >>> result1 + Result(x=1, y=2, w=99, beta=0.5) + + `result1` acts like a tuple of length 2: + + >>> len(result1) + 2 + >>> result1[:] + (1, 2) + + The values assigned when the instance was created are available as + attributes: + + >>> result1.y + 2 + >>> result1.beta + 0.5 + """ + if len(field_names) == 0: + raise ValueError('field_names must contain at least one name') + + if extra_field_names is None: + extra_field_names = [] + _validate_names(typename, field_names, extra_field_names) + + typename = _sys.intern(str(typename)) + field_names = tuple(map(_sys.intern, field_names)) + extra_field_names = tuple(map(_sys.intern, extra_field_names)) + + all_names = field_names + extra_field_names + arg_list = ', '.join(field_names) + full_list = ', '.join(all_names) + repr_fmt = ''.join(('(', + ', '.join(f'{name}=%({name})r' for name in all_names), + ')')) + tuple_new = tuple.__new__ + _dict, _tuple, _zip = dict, tuple, zip + + # Create all the named tuple methods to be added to the class namespace + + s = f"""\ +def __new__(_cls, {arg_list}, **extra_fields): + return _tuple_new(_cls, ({arg_list},)) + +def __init__(self, {arg_list}, **extra_fields): + for key in self._extra_fields: + if key not in extra_fields: + raise TypeError("missing keyword argument '%s'" % (key,)) + for key, val in extra_fields.items(): + if key not in self._extra_fields: + raise TypeError("unexpected keyword argument '%s'" % (key,)) + self.__dict__[key] = val + +def __setattr__(self, key, val): + if key in {repr(field_names)}: + raise AttributeError("can't set attribute %r of class %r" + % (key, self.__class__.__name__)) + else: + self.__dict__[key] = val +""" + del arg_list + namespace = {'_tuple_new': tuple_new, + '__builtins__': dict(TypeError=TypeError, + AttributeError=AttributeError), + '__name__': f'namedtuple_{typename}'} + exec(s, namespace) + __new__ = namespace['__new__'] + __new__.__doc__ = f'Create new instance of {typename}({full_list})' + __init__ = namespace['__init__'] + __init__.__doc__ = f'Instantiate instance of {typename}({full_list})' + __setattr__ = namespace['__setattr__'] + + def __repr__(self): + 'Return a nicely formatted representation string' + return self.__class__.__name__ + repr_fmt % self._asdict() + + def _asdict(self): + 'Return a new dict which maps field names to their values.' + out = _dict(_zip(self._fields, self)) + out.update(self.__dict__) + return out + + def __getnewargs_ex__(self): + 'Return self as a plain tuple. Used by copy and pickle.' + return _tuple(self), self.__dict__ + + # Modify function metadata to help with introspection and debugging + for method in (__new__, __repr__, _asdict, __getnewargs_ex__): + method.__qualname__ = f'{typename}.{method.__name__}' + + # Build-up the class namespace dictionary + # and use type() to build the result class + class_namespace = { + '__doc__': f'{typename}({full_list})', + '_fields': field_names, + '__new__': __new__, + '__init__': __init__, + '__repr__': __repr__, + '__setattr__': __setattr__, + '_asdict': _asdict, + '_extra_fields': extra_field_names, + '__getnewargs_ex__': __getnewargs_ex__, + } + for index, name in enumerate(field_names): + + def _get(self, index=index): + return self[index] + class_namespace[name] = property(_get) + for name in extra_field_names: + + def _get(self, name=name): + return self.__dict__[name] + class_namespace[name] = property(_get) + + result = type(typename, (tuple,), class_namespace) + + # For pickling to work, the __module__ variable needs to be set to the + # frame where the named tuple is created. Bypass this step in environments + # where sys._getframe is not defined (Jython for example) or sys._getframe + # is not defined for arguments greater than 0 (IronPython), or where the + # user has specified a particular module. + if module is None: + try: + module = _sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + pass + if module is not None: + result.__module__ = module + __new__.__module__ = module + + return result diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_ccallback.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_ccallback.py new file mode 100644 index 0000000000000000000000000000000000000000..1980d06f5489e6633fb611c35bfb56903bd63e7f --- /dev/null +++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_ccallback.py @@ -0,0 +1,251 @@ +from . import _ccallback_c + +import ctypes + +PyCFuncPtr = ctypes.CFUNCTYPE(ctypes.c_void_p).__bases__[0] + +ffi = None + +class CData: + pass + +def _import_cffi(): + global ffi, CData + + if ffi is not None: + return + + try: + import cffi + ffi = cffi.FFI() + CData = ffi.CData + except ImportError: + ffi = False + + +class LowLevelCallable(tuple): + """ + Low-level callback function. + + Some functions in SciPy take as arguments callback functions, which + can either be python callables or low-level compiled functions. Using + compiled callback functions can improve performance somewhat by + avoiding wrapping data in Python objects. + + Such low-level functions in SciPy are wrapped in `LowLevelCallable` + objects, which can be constructed from function pointers obtained from + ctypes, cffi, Cython, or contained in Python `PyCapsule` objects. + + .. seealso:: + + Functions accepting low-level callables: + + `scipy.integrate.quad`, `scipy.ndimage.generic_filter`, + `scipy.ndimage.generic_filter1d`, `scipy.ndimage.geometric_transform` + + Usage examples: + + :ref:`ndimage-ccallbacks`, :ref:`quad-callbacks` + + Parameters + ---------- + function : {PyCapsule, ctypes function pointer, cffi function pointer} + Low-level callback function. + user_data : {PyCapsule, ctypes void pointer, cffi void pointer} + User data to pass on to the callback function. + signature : str, optional + Signature of the function. If omitted, determined from *function*, + if possible. + + Attributes + ---------- + function + Callback function given. + user_data + User data given. + signature + Signature of the function. + + Methods + ------- + from_cython + Class method for constructing callables from Cython C-exported + functions. + + Notes + ----- + The argument ``function`` can be one of: + + - PyCapsule, whose name contains the C function signature + - ctypes function pointer + - cffi function pointer + + The signature of the low-level callback must match one of those expected + by the routine it is passed to. + + If constructing low-level functions from a PyCapsule, the name of the + capsule must be the corresponding signature, in the format:: + + return_type (arg1_type, arg2_type, ...) + + For example:: + + "void (double)" + "double (double, int *, void *)" + + The context of a PyCapsule passed in as ``function`` is used as ``user_data``, + if an explicit value for ``user_data`` was not given. + + """ + + # Make the class immutable + __slots__ = () + + def __new__(cls, function, user_data=None, signature=None): + # We need to hold a reference to the function & user data, + # to prevent them going out of scope + item = cls._parse_callback(function, user_data, signature) + return tuple.__new__(cls, (item, function, user_data)) + + def __repr__(self): + return f"LowLevelCallable({self.function!r}, {self.user_data!r})" + + @property + def function(self): + return tuple.__getitem__(self, 1) + + @property + def user_data(self): + return tuple.__getitem__(self, 2) + + @property + def signature(self): + return _ccallback_c.get_capsule_signature(tuple.__getitem__(self, 0)) + + def __getitem__(self, idx): + raise ValueError() + + @classmethod + def from_cython(cls, module, name, user_data=None, signature=None): + """ + Create a low-level callback function from an exported Cython function. + + Parameters + ---------- + module : module + Cython module where the exported function resides + name : str + Name of the exported function + user_data : {PyCapsule, ctypes void pointer, cffi void pointer}, optional + User data to pass on to the callback function. + signature : str, optional + Signature of the function. If omitted, determined from *function*. + + """ + try: + function = module.__pyx_capi__[name] + except AttributeError as e: + message = "Given module is not a Cython module with __pyx_capi__ attribute" + raise ValueError(message) from e + except KeyError as e: + message = f"No function {name!r} found in __pyx_capi__ of the module" + raise ValueError(message) from e + return cls(function, user_data, signature) + + @classmethod + def _parse_callback(cls, obj, user_data=None, signature=None): + _import_cffi() + + if isinstance(obj, LowLevelCallable): + func = tuple.__getitem__(obj, 0) + elif isinstance(obj, PyCFuncPtr): + func, signature = _get_ctypes_func(obj, signature) + elif isinstance(obj, CData): + func, signature = _get_cffi_func(obj, signature) + elif _ccallback_c.check_capsule(obj): + func = obj + else: + raise ValueError("Given input is not a callable or a " + "low-level callable (pycapsule/ctypes/cffi)") + + if isinstance(user_data, ctypes.c_void_p): + context = _get_ctypes_data(user_data) + elif isinstance(user_data, CData): + context = _get_cffi_data(user_data) + elif user_data is None: + context = 0 + elif _ccallback_c.check_capsule(user_data): + context = user_data + else: + raise ValueError("Given user data is not a valid " + "low-level void* pointer (pycapsule/ctypes/cffi)") + + return _ccallback_c.get_raw_capsule(func, signature, context) + + +# +# ctypes helpers +# + +def _get_ctypes_func(func, signature=None): + # Get function pointer + func_ptr = ctypes.cast(func, ctypes.c_void_p).value + + # Construct function signature + if signature is None: + signature = _typename_from_ctypes(func.restype) + " (" + for j, arg in enumerate(func.argtypes): + if j == 0: + signature += _typename_from_ctypes(arg) + else: + signature += ", " + _typename_from_ctypes(arg) + signature += ")" + + return func_ptr, signature + + +def _typename_from_ctypes(item): + if item is None: + return "void" + elif item is ctypes.c_void_p: + return "void *" + + name = item.__name__ + + pointer_level = 0 + while name.startswith("LP_"): + pointer_level += 1 + name = name[3:] + + if name.startswith('c_'): + name = name[2:] + + if pointer_level > 0: + name += " " + "*"*pointer_level + + return name + + +def _get_ctypes_data(data): + # Get voidp pointer + return ctypes.cast(data, ctypes.c_void_p).value + + +# +# CFFI helpers +# + +def _get_cffi_func(func, signature=None): + # Get function pointer + func_ptr = ffi.cast('uintptr_t', func) + + # Get signature + if signature is None: + signature = ffi.getctype(ffi.typeof(func)).replace('(*)', ' ') + + return func_ptr, signature + + +def _get_cffi_data(data): + # Get pointer + return ffi.cast('uintptr_t', data) diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_disjoint_set.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_disjoint_set.py new file mode 100644 index 0000000000000000000000000000000000000000..683c5c8e518705e710212dafc01363f92a2f947d --- /dev/null +++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_disjoint_set.py @@ -0,0 +1,254 @@ +""" +Disjoint set data structure +""" + + +class DisjointSet: + """ Disjoint set data structure for incremental connectivity queries. + + .. versionadded:: 1.6.0 + + Attributes + ---------- + n_subsets : int + The number of subsets. + + Methods + ------- + add + merge + connected + subset + subset_size + subsets + __getitem__ + + Notes + ----- + This class implements the disjoint set [1]_, also known as the *union-find* + or *merge-find* data structure. The *find* operation (implemented in + `__getitem__`) implements the *path halving* variant. The *merge* method + implements the *merge by size* variant. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Disjoint-set_data_structure + + Examples + -------- + >>> from scipy.cluster.hierarchy import DisjointSet + + Initialize a disjoint set: + + >>> disjoint_set = DisjointSet([1, 2, 3, 'a', 'b']) + + Merge some subsets: + + >>> disjoint_set.merge(1, 2) + True + >>> disjoint_set.merge(3, 'a') + True + >>> disjoint_set.merge('a', 'b') + True + >>> disjoint_set.merge('b', 'b') + False + + Find root elements: + + >>> disjoint_set[2] + 1 + >>> disjoint_set['b'] + 3 + + Test connectivity: + + >>> disjoint_set.connected(1, 2) + True + >>> disjoint_set.connected(1, 'b') + False + + List elements in disjoint set: + + >>> list(disjoint_set) + [1, 2, 3, 'a', 'b'] + + Get the subset containing 'a': + + >>> disjoint_set.subset('a') + {'a', 3, 'b'} + + Get the size of the subset containing 'a' (without actually instantiating + the subset): + + >>> disjoint_set.subset_size('a') + 3 + + Get all subsets in the disjoint set: + + >>> disjoint_set.subsets() + [{1, 2}, {'a', 3, 'b'}] + """ + def __init__(self, elements=None): + self.n_subsets = 0 + self._sizes = {} + self._parents = {} + # _nbrs is a circular linked list which links connected elements. + self._nbrs = {} + # _indices tracks the element insertion order in `__iter__`. + self._indices = {} + if elements is not None: + for x in elements: + self.add(x) + + def __iter__(self): + """Returns an iterator of the elements in the disjoint set. + + Elements are ordered by insertion order. + """ + return iter(self._indices) + + def __len__(self): + return len(self._indices) + + def __contains__(self, x): + return x in self._indices + + def __getitem__(self, x): + """Find the root element of `x`. + + Parameters + ---------- + x : hashable object + Input element. + + Returns + ------- + root : hashable object + Root element of `x`. + """ + if x not in self._indices: + raise KeyError(x) + + # find by "path halving" + parents = self._parents + while self._indices[x] != self._indices[parents[x]]: + parents[x] = parents[parents[x]] + x = parents[x] + return x + + def add(self, x): + """Add element `x` to disjoint set + """ + if x in self._indices: + return + + self._sizes[x] = 1 + self._parents[x] = x + self._nbrs[x] = x + self._indices[x] = len(self._indices) + self.n_subsets += 1 + + def merge(self, x, y): + """Merge the subsets of `x` and `y`. + + The smaller subset (the child) is merged into the larger subset (the + parent). If the subsets are of equal size, the root element which was + first inserted into the disjoint set is selected as the parent. + + Parameters + ---------- + x, y : hashable object + Elements to merge. + + Returns + ------- + merged : bool + True if `x` and `y` were in disjoint sets, False otherwise. + """ + xr = self[x] + yr = self[y] + if self._indices[xr] == self._indices[yr]: + return False + + sizes = self._sizes + if (sizes[xr], self._indices[yr]) < (sizes[yr], self._indices[xr]): + xr, yr = yr, xr + self._parents[yr] = xr + self._sizes[xr] += self._sizes[yr] + self._nbrs[xr], self._nbrs[yr] = self._nbrs[yr], self._nbrs[xr] + self.n_subsets -= 1 + return True + + def connected(self, x, y): + """Test whether `x` and `y` are in the same subset. + + Parameters + ---------- + x, y : hashable object + Elements to test. + + Returns + ------- + result : bool + True if `x` and `y` are in the same set, False otherwise. + """ + return self._indices[self[x]] == self._indices[self[y]] + + def subset(self, x): + """Get the subset containing `x`. + + Parameters + ---------- + x : hashable object + Input element. + + Returns + ------- + result : set + Subset containing `x`. + """ + if x not in self._indices: + raise KeyError(x) + + result = [x] + nxt = self._nbrs[x] + while self._indices[nxt] != self._indices[x]: + result.append(nxt) + nxt = self._nbrs[nxt] + return set(result) + + def subset_size(self, x): + """Get the size of the subset containing `x`. + + Note that this method is faster than ``len(self.subset(x))`` because + the size is directly read off an internal field, without the need to + instantiate the full subset. + + Parameters + ---------- + x : hashable object + Input element. + + Returns + ------- + result : int + Size of the subset containing `x`. + """ + return self._sizes[self[x]] + + def subsets(self): + """Get all the subsets in the disjoint set. + + Returns + ------- + result : list + Subsets in the disjoint set. + """ + result = [] + visited = set() + for x in self: + if x not in visited: + xset = self.subset(x) + visited.update(xset) + result.append(xset) + return result diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_docscrape.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_docscrape.py new file mode 100644 index 0000000000000000000000000000000000000000..f5ad8058366fa4df2f2fa870de7e4f0eff79b1e1 --- /dev/null +++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_docscrape.py @@ -0,0 +1,679 @@ +"""Extract reference documentation from the NumPy source tree. + +""" +# copied from numpydoc/docscrape.py +import inspect +import textwrap +import re +import pydoc +from warnings import warn +from collections import namedtuple +from collections.abc import Callable, Mapping +import copy +import sys + + +def strip_blank_lines(l): + "Remove leading and trailing blank lines from a list of lines" + while l and not l[0].strip(): + del l[0] + while l and not l[-1].strip(): + del l[-1] + return l + + +class Reader: + """A line-based string reader. + + """ + def __init__(self, data): + """ + Parameters + ---------- + data : str + String with lines separated by '\\n'. + + """ + if isinstance(data, list): + self._str = data + else: + self._str = data.split('\n') # store string as list of lines + + self.reset() + + def __getitem__(self, n): + return self._str[n] + + def reset(self): + self._l = 0 # current line nr + + def read(self): + if not self.eof(): + out = self[self._l] + self._l += 1 + return out + else: + return '' + + def seek_next_non_empty_line(self): + for l in self[self._l:]: + if l.strip(): + break + else: + self._l += 1 + + def eof(self): + return self._l >= len(self._str) + + def read_to_condition(self, condition_func): + start = self._l + for line in self[start:]: + if condition_func(line): + return self[start:self._l] + self._l += 1 + if self.eof(): + return self[start:self._l+1] + return [] + + def read_to_next_empty_line(self): + self.seek_next_non_empty_line() + + def is_empty(line): + return not line.strip() + + return self.read_to_condition(is_empty) + + def read_to_next_unindented_line(self): + def is_unindented(line): + return (line.strip() and (len(line.lstrip()) == len(line))) + return self.read_to_condition(is_unindented) + + def peek(self, n=0): + if self._l + n < len(self._str): + return self[self._l + n] + else: + return '' + + def is_empty(self): + return not ''.join(self._str).strip() + + +class ParseError(Exception): + def __str__(self): + message = self.args[0] + if hasattr(self, 'docstring'): + message = f"{message} in {self.docstring!r}" + return message + + +Parameter = namedtuple('Parameter', ['name', 'type', 'desc']) + + +class NumpyDocString(Mapping): + """Parses a numpydoc string to an abstract representation + + Instances define a mapping from section title to structured data. + + """ + + sections = { + 'Signature': '', + 'Summary': [''], + 'Extended Summary': [], + 'Parameters': [], + 'Returns': [], + 'Yields': [], + 'Receives': [], + 'Raises': [], + 'Warns': [], + 'Other Parameters': [], + 'Attributes': [], + 'Methods': [], + 'See Also': [], + 'Notes': [], + 'Warnings': [], + 'References': '', + 'Examples': '', + 'index': {} + } + + def __init__(self, docstring, config={}): + orig_docstring = docstring + docstring = textwrap.dedent(docstring).split('\n') + + self._doc = Reader(docstring) + self._parsed_data = copy.deepcopy(self.sections) + + try: + self._parse() + except ParseError as e: + e.docstring = orig_docstring + raise + + def __getitem__(self, key): + return self._parsed_data[key] + + def __setitem__(self, key, val): + if key not in self._parsed_data: + self._error_location("Unknown section %s" % key, error=False) + else: + self._parsed_data[key] = val + + def __iter__(self): + return iter(self._parsed_data) + + def __len__(self): + return len(self._parsed_data) + + def _is_at_section(self): + self._doc.seek_next_non_empty_line() + + if self._doc.eof(): + return False + + l1 = self._doc.peek().strip() # e.g. Parameters + + if l1.startswith('.. index::'): + return True + + l2 = self._doc.peek(1).strip() # ---------- or ========== + return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1)) + + def _strip(self, doc): + i = 0 + j = 0 + for i, line in enumerate(doc): + if line.strip(): + break + + for j, line in enumerate(doc[::-1]): + if line.strip(): + break + + return doc[i:len(doc)-j] + + def _read_to_next_section(self): + section = self._doc.read_to_next_empty_line() + + while not self._is_at_section() and not self._doc.eof(): + if not self._doc.peek(-1).strip(): # previous line was empty + section += [''] + + section += self._doc.read_to_next_empty_line() + + return section + + def _read_sections(self): + while not self._doc.eof(): + data = self._read_to_next_section() + name = data[0].strip() + + if name.startswith('..'): # index section + yield name, data[1:] + elif len(data) < 2: + yield StopIteration + else: + yield name, self._strip(data[2:]) + + def _parse_param_list(self, content, single_element_is_type=False): + r = Reader(content) + params = [] + while not r.eof(): + header = r.read().strip() + if ' : ' in header: + arg_name, arg_type = header.split(' : ')[:2] + else: + if single_element_is_type: + arg_name, arg_type = '', header + else: + arg_name, arg_type = header, '' + + desc = r.read_to_next_unindented_line() + desc = dedent_lines(desc) + desc = strip_blank_lines(desc) + + params.append(Parameter(arg_name, arg_type, desc)) + + return params + + # See also supports the following formats. + # + # + # SPACE* COLON SPACE+ SPACE* + # ( COMMA SPACE+ )+ (COMMA | PERIOD)? SPACE* + # ( COMMA SPACE+ )* SPACE* COLON SPACE+ SPACE* + + # is one of + # + # COLON COLON BACKTICK BACKTICK + # where + # is a legal function name, and + # is any nonempty sequence of word characters. + # Examples: func_f1 :meth:`func_h1` :obj:`~baz.obj_r` :class:`class_j` + # is a string describing the function. + + _role = r":(?P\w+):" + _funcbacktick = r"`(?P(?:~\w+\.)?[a-zA-Z0-9_\.-]+)`" + _funcplain = r"(?P[a-zA-Z0-9_\.-]+)" + _funcname = r"(" + _role + _funcbacktick + r"|" + _funcplain + r")" + _funcnamenext = _funcname.replace('role', 'rolenext') + _funcnamenext = _funcnamenext.replace('name', 'namenext') + _description = r"(?P\s*:(\s+(?P\S+.*))?)?\s*$" + _func_rgx = re.compile(r"^\s*" + _funcname + r"\s*") + _line_rgx = re.compile( + r"^\s*" + + r"(?P" + # group for all function names + _funcname + + r"(?P([,]\s+" + _funcnamenext + r")*)" + + r")" + # end of "allfuncs" + # Some function lists have a trailing comma (or period) '\s*' + r"(?P[,\.])?" + + _description) + + # Empty elements are replaced with '..' + empty_description = '..' + + def _parse_see_also(self, content): + """ + func_name : Descriptive text + continued text + another_func_name : Descriptive text + func_name1, func_name2, :meth:`func_name`, func_name3 + + """ + + items = [] + + def parse_item_name(text): + """Match ':role:`name`' or 'name'.""" + m = self._func_rgx.match(text) + if not m: + raise ParseError("%s is not a item name" % text) + role = m.group('role') + name = m.group('name') if role else m.group('name2') + return name, role, m.end() + + rest = [] + for line in content: + if not line.strip(): + continue + + line_match = self._line_rgx.match(line) + description = None + if line_match: + description = line_match.group('desc') + if line_match.group('trailing') and description: + self._error_location( + 'Unexpected comma or period after function list at ' + 'index %d of line "%s"' % (line_match.end('trailing'), + line), + error=False) + if not description and line.startswith(' '): + rest.append(line.strip()) + elif line_match: + funcs = [] + text = line_match.group('allfuncs') + while True: + if not text.strip(): + break + name, role, match_end = parse_item_name(text) + funcs.append((name, role)) + text = text[match_end:].strip() + if text and text[0] == ',': + text = text[1:].strip() + rest = list(filter(None, [description])) + items.append((funcs, rest)) + else: + raise ParseError("%s is not a item name" % line) + return items + + def _parse_index(self, section, content): + """ + .. index:: default + :refguide: something, else, and more + + """ + def strip_each_in(lst): + return [s.strip() for s in lst] + + out = {} + section = section.split('::') + if len(section) > 1: + out['default'] = strip_each_in(section[1].split(','))[0] + for line in content: + line = line.split(':') + if len(line) > 2: + out[line[1]] = strip_each_in(line[2].split(',')) + return out + + def _parse_summary(self): + """Grab signature (if given) and summary""" + if self._is_at_section(): + return + + # If several signatures present, take the last one + while True: + summary = self._doc.read_to_next_empty_line() + summary_str = " ".join([s.strip() for s in summary]).strip() + compiled = re.compile(r'^([\w., ]+=)?\s*[\w\.]+\(.*\)$') + if compiled.match(summary_str): + self['Signature'] = summary_str + if not self._is_at_section(): + continue + break + + if summary is not None: + self['Summary'] = summary + + if not self._is_at_section(): + self['Extended Summary'] = self._read_to_next_section() + + def _parse(self): + self._doc.reset() + self._parse_summary() + + sections = list(self._read_sections()) + section_names = {section for section, content in sections} + + has_returns = 'Returns' in section_names + has_yields = 'Yields' in section_names + # We could do more tests, but we are not. Arbitrarily. + if has_returns and has_yields: + msg = 'Docstring contains both a Returns and Yields section.' + raise ValueError(msg) + if not has_yields and 'Receives' in section_names: + msg = 'Docstring contains a Receives section but not Yields.' + raise ValueError(msg) + + for (section, content) in sections: + if not section.startswith('..'): + section = (s.capitalize() for s in section.split(' ')) + section = ' '.join(section) + if self.get(section): + self._error_location("The section %s appears twice" + % section) + + if section in ('Parameters', 'Other Parameters', 'Attributes', + 'Methods'): + self[section] = self._parse_param_list(content) + elif section in ('Returns', 'Yields', 'Raises', 'Warns', + 'Receives'): + self[section] = self._parse_param_list( + content, single_element_is_type=True) + elif section.startswith('.. index::'): + self['index'] = self._parse_index(section, content) + elif section == 'See Also': + self['See Also'] = self._parse_see_also(content) + else: + self[section] = content + + def _error_location(self, msg, error=True): + if hasattr(self, '_obj'): + # we know where the docs came from: + try: + filename = inspect.getsourcefile(self._obj) + except TypeError: + filename = None + msg = msg + (f" in the docstring of {self._obj} in {filename}.") + if error: + raise ValueError(msg) + else: + warn(msg, stacklevel=3) + + # string conversion routines + + def _str_header(self, name, symbol='-'): + return [name, len(name)*symbol] + + def _str_indent(self, doc, indent=4): + out = [] + for line in doc: + out += [' '*indent + line] + return out + + def _str_signature(self): + if self['Signature']: + return [self['Signature'].replace('*', r'\*')] + [''] + else: + return [''] + + def _str_summary(self): + if self['Summary']: + return self['Summary'] + [''] + else: + return [] + + def _str_extended_summary(self): + if self['Extended Summary']: + return self['Extended Summary'] + [''] + else: + return [] + + def _str_param_list(self, name): + out = [] + if self[name]: + out += self._str_header(name) + for param in self[name]: + parts = [] + if param.name: + parts.append(param.name) + if param.type: + parts.append(param.type) + out += [' : '.join(parts)] + if param.desc and ''.join(param.desc).strip(): + out += self._str_indent(param.desc) + out += [''] + return out + + def _str_section(self, name): + out = [] + if self[name]: + out += self._str_header(name) + out += self[name] + out += [''] + return out + + def _str_see_also(self, func_role): + if not self['See Also']: + return [] + out = [] + out += self._str_header("See Also") + out += [''] + last_had_desc = True + for funcs, desc in self['See Also']: + assert isinstance(funcs, list) + links = [] + for func, role in funcs: + if role: + link = f':{role}:`{func}`' + elif func_role: + link = f':{func_role}:`{func}`' + else: + link = "`%s`_" % func + links.append(link) + link = ', '.join(links) + out += [link] + if desc: + out += self._str_indent([' '.join(desc)]) + last_had_desc = True + else: + last_had_desc = False + out += self._str_indent([self.empty_description]) + + if last_had_desc: + out += [''] + out += [''] + return out + + def _str_index(self): + idx = self['index'] + out = [] + output_index = False + default_index = idx.get('default', '') + if default_index: + output_index = True + out += ['.. index:: %s' % default_index] + for section, references in idx.items(): + if section == 'default': + continue + output_index = True + out += [' :{}: {}'.format(section, ', '.join(references))] + if output_index: + return out + else: + return '' + + def __str__(self, func_role=''): + out = [] + out += self._str_signature() + out += self._str_summary() + out += self._str_extended_summary() + for param_list in ('Parameters', 'Returns', 'Yields', 'Receives', + 'Other Parameters', 'Raises', 'Warns'): + out += self._str_param_list(param_list) + out += self._str_section('Warnings') + out += self._str_see_also(func_role) + for s in ('Notes', 'References', 'Examples'): + out += self._str_section(s) + for param_list in ('Attributes', 'Methods'): + out += self._str_param_list(param_list) + out += self._str_index() + return '\n'.join(out) + + +def indent(str, indent=4): + indent_str = ' '*indent + if str is None: + return indent_str + lines = str.split('\n') + return '\n'.join(indent_str + l for l in lines) + + +def dedent_lines(lines): + """Deindent a list of lines maximally""" + return textwrap.dedent("\n".join(lines)).split("\n") + + +def header(text, style='-'): + return text + '\n' + style*len(text) + '\n' + + +class FunctionDoc(NumpyDocString): + def __init__(self, func, role='func', doc=None, config={}): + self._f = func + self._role = role # e.g. "func" or "meth" + + if doc is None: + if func is None: + raise ValueError("No function or docstring given") + doc = inspect.getdoc(func) or '' + NumpyDocString.__init__(self, doc, config) + + def get_func(self): + func_name = getattr(self._f, '__name__', self.__class__.__name__) + if inspect.isclass(self._f): + func = getattr(self._f, '__call__', self._f.__init__) + else: + func = self._f + return func, func_name + + def __str__(self): + out = '' + + func, func_name = self.get_func() + + roles = {'func': 'function', + 'meth': 'method'} + + if self._role: + if self._role not in roles: + print("Warning: invalid role %s" % self._role) + out += '.. {}:: {}\n \n\n'.format(roles.get(self._role, ''), + func_name) + + out += super().__str__(func_role=self._role) + return out + + +class ClassDoc(NumpyDocString): + + extra_public_methods = ['__call__'] + + def __init__(self, cls, doc=None, modulename='', func_doc=FunctionDoc, + config={}): + if not inspect.isclass(cls) and cls is not None: + raise ValueError("Expected a class or None, but got %r" % cls) + self._cls = cls + + if 'sphinx' in sys.modules: + from sphinx.ext.autodoc import ALL + else: + ALL = object() + + self.show_inherited_members = config.get( + 'show_inherited_class_members', True) + + if modulename and not modulename.endswith('.'): + modulename += '.' + self._mod = modulename + + if doc is None: + if cls is None: + raise ValueError("No class or documentation string given") + doc = pydoc.getdoc(cls) + + NumpyDocString.__init__(self, doc) + + _members = config.get('members', []) + if _members is ALL: + _members = None + _exclude = config.get('exclude-members', []) + + if config.get('show_class_members', True) and _exclude is not ALL: + def splitlines_x(s): + if not s: + return [] + else: + return s.splitlines() + for field, items in [('Methods', self.methods), + ('Attributes', self.properties)]: + if not self[field]: + doc_list = [] + for name in sorted(items): + if (name in _exclude or + (_members and name not in _members)): + continue + try: + doc_item = pydoc.getdoc(getattr(self._cls, name)) + doc_list.append( + Parameter(name, '', splitlines_x(doc_item))) + except AttributeError: + pass # method doesn't exist + self[field] = doc_list + + @property + def methods(self): + if self._cls is None: + return [] + return [name for name, func in inspect.getmembers(self._cls) + if ((not name.startswith('_') + or name in self.extra_public_methods) + and isinstance(func, Callable) + and self._is_show_member(name))] + + @property + def properties(self): + if self._cls is None: + return [] + return [name for name, func in inspect.getmembers(self._cls) + if (not name.startswith('_') and + (func is None or isinstance(func, property) or + inspect.isdatadescriptor(func)) + and self._is_show_member(name))] + + def _is_show_member(self, name): + if self.show_inherited_members: + return True # show all class members + if name not in self._cls.__dict__: + return False # class member is inherited, we do not show it + return True diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_elementwise_iterative_method.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_elementwise_iterative_method.py new file mode 100644 index 0000000000000000000000000000000000000000..c82363b754489607b53a430201cffb42ee881a76 --- /dev/null +++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_elementwise_iterative_method.py @@ -0,0 +1,348 @@ +# `_elementwise_iterative_method.py` includes tools for writing functions that +# - are vectorized to work elementwise on arrays, +# - implement non-trivial, iterative algorithms with a callback interface, and +# - return rich objects with iteration count, termination status, etc. +# +# Examples include: +# `scipy.optimize._chandrupatla._chandrupatla for scalar rootfinding, +# `scipy.optimize._chandrupatla._chandrupatla_minimize for scalar minimization, +# `scipy.optimize._differentiate._differentiate for numerical differentiation, +# `scipy.optimize._bracket._bracket_root for finding rootfinding brackets, +# `scipy.optimize._bracket._bracket_minimize for finding minimization brackets, +# `scipy.integrate._tanhsinh._tanhsinh` for numerical quadrature. + +import math +import numpy as np +from ._util import _RichResult, _call_callback_maybe_halt +from ._array_api import array_namespace, size as xp_size + +_ESIGNERR = -1 +_ECONVERR = -2 +_EVALUEERR = -3 +_ECALLBACK = -4 +_EINPUTERR = -5 +_ECONVERGED = 0 +_EINPROGRESS = 1 + +def _initialize(func, xs, args, complex_ok=False, preserve_shape=None): + """Initialize abscissa, function, and args arrays for elementwise function + + Parameters + ---------- + func : callable + An elementwise function with signature + + func(x: ndarray, *args) -> ndarray + + where each element of ``x`` is a finite real and ``args`` is a tuple, + which may contain an arbitrary number of arrays that are broadcastable + with ``x``. + xs : tuple of arrays + Finite real abscissa arrays. Must be broadcastable. + args : tuple, optional + Additional positional arguments to be passed to `func`. + preserve_shape : bool, default:False + When ``preserve_shape=False`` (default), `func` may be passed + arguments of any shape; `_scalar_optimization_loop` is permitted + to reshape and compress arguments at will. When + ``preserve_shape=False``, arguments passed to `func` must have shape + `shape` or ``shape + (n,)``, where ``n`` is any integer. + + Returns + ------- + xs, fs, args : tuple of arrays + Broadcasted, writeable, 1D abscissa and function value arrays (or + NumPy floats, if appropriate). The dtypes of the `xs` and `fs` are + `xfat`; the dtype of the `args` are unchanged. + shape : tuple of ints + Original shape of broadcasted arrays. + xfat : NumPy dtype + Result dtype of abscissae, function values, and args determined using + `np.result_type`, except integer types are promoted to `np.float64`. + + Raises + ------ + ValueError + If the result dtype is not that of a real scalar + + Notes + ----- + Useful for initializing the input of SciPy functions that accept + an elementwise callable, abscissae, and arguments; e.g. + `scipy.optimize._chandrupatla`. + """ + nx = len(xs) + xp = array_namespace(*xs) + + # Try to preserve `dtype`, but we need to ensure that the arguments are at + # least floats before passing them into the function; integers can overflow + # and cause failure. + # There might be benefit to combining the `xs` into a single array and + # calling `func` once on the combined array. For now, keep them separate. + xas = xp.broadcast_arrays(*xs, *args) # broadcast and rename + xat = xp.result_type(*[xa.dtype for xa in xas]) + xat = xp.asarray(1.).dtype if xp.isdtype(xat, "integral") else xat + xs, args = xas[:nx], xas[nx:] + xs = [xp.asarray(x, dtype=xat) for x in xs] # use copy=False when implemented + fs = [xp.asarray(func(x, *args)) for x in xs] + shape = xs[0].shape + fshape = fs[0].shape + + if preserve_shape: + # bind original shape/func now to avoid late-binding gotcha + def func(x, *args, shape=shape, func=func, **kwargs): + i = (0,)*(len(fshape) - len(shape)) + return func(x[i], *args, **kwargs) + shape = np.broadcast_shapes(fshape, shape) # just shapes; use of NumPy OK + xs = [xp.broadcast_to(x, shape) for x in xs] + args = [xp.broadcast_to(arg, shape) for arg in args] + + message = ("The shape of the array returned by `func` must be the same as " + "the broadcasted shape of `x` and all other `args`.") + if preserve_shape is not None: # only in tanhsinh for now + message = f"When `preserve_shape=False`, {message.lower()}" + shapes_equal = [f.shape == shape for f in fs] + if not all(shapes_equal): # use Python all to reduce overhead + raise ValueError(message) + + # These algorithms tend to mix the dtypes of the abscissae and function + # values, so figure out what the result will be and convert them all to + # that type from the outset. + xfat = xp.result_type(*([f.dtype for f in fs] + [xat])) + if not complex_ok and not xp.isdtype(xfat, "real floating"): + raise ValueError("Abscissae and function output must be real numbers.") + xs = [xp.asarray(x, dtype=xfat, copy=True) for x in xs] + fs = [xp.asarray(f, dtype=xfat, copy=True) for f in fs] + + # To ensure that we can do indexing, we'll work with at least 1d arrays, + # but remember the appropriate shape of the output. + xs = [xp.reshape(x, (-1,)) for x in xs] + fs = [xp.reshape(f, (-1,)) for f in fs] + args = [xp.reshape(xp.asarray(arg, copy=True), (-1,)) for arg in args] + return func, xs, fs, args, shape, xfat, xp + + +def _loop(work, callback, shape, maxiter, func, args, dtype, pre_func_eval, + post_func_eval, check_termination, post_termination_check, + customize_result, res_work_pairs, xp, preserve_shape=False): + """Main loop of a vectorized scalar optimization algorithm + + Parameters + ---------- + work : _RichResult + All variables that need to be retained between iterations. Must + contain attributes `nit`, `nfev`, and `success` + callback : callable + User-specified callback function + shape : tuple of ints + The shape of all output arrays + maxiter : + Maximum number of iterations of the algorithm + func : callable + The user-specified callable that is being optimized or solved + args : tuple + Additional positional arguments to be passed to `func`. + dtype : NumPy dtype + The common dtype of all abscissae and function values + pre_func_eval : callable + A function that accepts `work` and returns `x`, the active elements + of `x` at which `func` will be evaluated. May modify attributes + of `work` with any algorithmic steps that need to happen + at the beginning of an iteration, before `func` is evaluated, + post_func_eval : callable + A function that accepts `x`, `func(x)`, and `work`. May modify + attributes of `work` with any algorithmic steps that need to happen + in the middle of an iteration, after `func` is evaluated but before + the termination check. + check_termination : callable + A function that accepts `work` and returns `stop`, a boolean array + indicating which of the active elements have met a termination + condition. + post_termination_check : callable + A function that accepts `work`. May modify `work` with any algorithmic + steps that need to happen after the termination check and before the + end of the iteration. + customize_result : callable + A function that accepts `res` and `shape` and returns `shape`. May + modify `res` (in-place) according to preferences (e.g. rearrange + elements between attributes) and modify `shape` if needed. + res_work_pairs : list of (str, str) + Identifies correspondence between attributes of `res` and attributes + of `work`; i.e., attributes of active elements of `work` will be + copied to the appropriate indices of `res` when appropriate. The order + determines the order in which _RichResult attributes will be + pretty-printed. + + Returns + ------- + res : _RichResult + The final result object + + Notes + ----- + Besides providing structure, this framework provides several important + services for a vectorized optimization algorithm. + + - It handles common tasks involving iteration count, function evaluation + count, a user-specified callback, and associated termination conditions. + - It compresses the attributes of `work` to eliminate unnecessary + computation on elements that have already converged. + + """ + if xp is None: + raise NotImplementedError("Must provide xp.") + + cb_terminate = False + + # Initialize the result object and active element index array + n_elements = math.prod(shape) + active = xp.arange(n_elements) # in-progress element indices + res_dict = {i: xp.zeros(n_elements, dtype=dtype) for i, j in res_work_pairs} + res_dict['success'] = xp.zeros(n_elements, dtype=xp.bool) + res_dict['status'] = xp.full(n_elements, _EINPROGRESS, dtype=xp.int32) + res_dict['nit'] = xp.zeros(n_elements, dtype=xp.int32) + res_dict['nfev'] = xp.zeros(n_elements, dtype=xp.int32) + res = _RichResult(res_dict) + work.args = args + + active = _check_termination(work, res, res_work_pairs, active, + check_termination, preserve_shape, xp) + + if callback is not None: + temp = _prepare_result(work, res, res_work_pairs, active, shape, + customize_result, preserve_shape, xp) + if _call_callback_maybe_halt(callback, temp): + cb_terminate = True + + while work.nit < maxiter and xp_size(active) and not cb_terminate and n_elements: + x = pre_func_eval(work) + + if work.args and work.args[0].ndim != x.ndim: + # `x` always starts as 1D. If the SciPy function that uses + # _loop added dimensions to `x`, we need to + # add them to the elements of `args`. + args = [] + for arg in work.args: + n_new_dims = x.ndim - arg.ndim + new_shape = arg.shape + (1,)*n_new_dims + args.append(xp.reshape(arg, new_shape)) + work.args = args + + x_shape = x.shape + if preserve_shape: + x = xp.reshape(x, (shape + (-1,))) + f = func(x, *work.args) + f = xp.asarray(f, dtype=dtype) + if preserve_shape: + x = xp.reshape(x, x_shape) + f = xp.reshape(f, x_shape) + work.nfev += 1 if x.ndim == 1 else x.shape[-1] + + post_func_eval(x, f, work) + + work.nit += 1 + active = _check_termination(work, res, res_work_pairs, active, + check_termination, preserve_shape, xp) + + if callback is not None: + temp = _prepare_result(work, res, res_work_pairs, active, shape, + customize_result, preserve_shape, xp) + if _call_callback_maybe_halt(callback, temp): + cb_terminate = True + break + if xp_size(active) == 0: + break + + post_termination_check(work) + + work.status[:] = _ECALLBACK if cb_terminate else _ECONVERR + return _prepare_result(work, res, res_work_pairs, active, shape, + customize_result, preserve_shape, xp) + + +def _check_termination(work, res, res_work_pairs, active, check_termination, + preserve_shape, xp): + # Checks termination conditions, updates elements of `res` with + # corresponding elements of `work`, and compresses `work`. + + stop = check_termination(work) + + if xp.any(stop): + # update the active elements of the result object with the active + # elements for which a termination condition has been met + _update_active(work, res, res_work_pairs, active, stop, preserve_shape, xp) + + if preserve_shape: + stop = stop[active] + + proceed = ~stop + active = active[proceed] + + if not preserve_shape: + # compress the arrays to avoid unnecessary computation + for key, val in work.items(): + # Need to find a better way than these try/excepts + # Somehow need to keep compressible numerical args separate + if key == 'args': + continue + try: + work[key] = val[proceed] + except (IndexError, TypeError, KeyError): # not a compressible array + work[key] = val + work.args = [arg[proceed] for arg in work.args] + + return active + + +def _update_active(work, res, res_work_pairs, active, mask, preserve_shape, xp): + # Update `active` indices of the arrays in result object `res` with the + # contents of the scalars and arrays in `update_dict`. When provided, + # `mask` is a boolean array applied both to the arrays in `update_dict` + # that are to be used and to the arrays in `res` that are to be updated. + update_dict = {key1: work[key2] for key1, key2 in res_work_pairs} + update_dict['success'] = work.status == 0 + + if mask is not None: + if preserve_shape: + active_mask = xp.zeros_like(mask) + active_mask[active] = 1 + active_mask = active_mask & mask + for key, val in update_dict.items(): + try: + res[key][active_mask] = val[active_mask] + except (IndexError, TypeError, KeyError): + res[key][active_mask] = val + else: + active_mask = active[mask] + for key, val in update_dict.items(): + try: + res[key][active_mask] = val[mask] + except (IndexError, TypeError, KeyError): + res[key][active_mask] = val + else: + for key, val in update_dict.items(): + if preserve_shape: + try: + val = val[active] + except (IndexError, TypeError, KeyError): + pass + res[key][active] = val + + +def _prepare_result(work, res, res_work_pairs, active, shape, customize_result, + preserve_shape, xp): + # Prepare the result object `res` by creating a copy, copying the latest + # data from work, running the provided result customization function, + # and reshaping the data to the original shapes. + res = res.copy() + _update_active(work, res, res_work_pairs, active, None, preserve_shape, xp) + + shape = customize_result(res, shape) + + for key, val in res.items(): + # this looks like it won't work for xp != np if val is not numeric + temp = xp.reshape(val, shape) + res[key] = temp[()] if temp.ndim == 0 else temp + + res['_order_keys'] = ['success'] + [i for i, j in res_work_pairs] + return _RichResult(**res) diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_finite_differences.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_finite_differences.py new file mode 100644 index 0000000000000000000000000000000000000000..506057b48b3f49244e1ed6cd755fad8ad43d8739 --- /dev/null +++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_finite_differences.py @@ -0,0 +1,145 @@ +from numpy import arange, newaxis, hstack, prod, array + + +def _central_diff_weights(Np, ndiv=1): + """ + Return weights for an Np-point central derivative. + + Assumes equally-spaced function points. + + If weights are in the vector w, then + derivative is w[0] * f(x-ho*dx) + ... + w[-1] * f(x+h0*dx) + + Parameters + ---------- + Np : int + Number of points for the central derivative. + ndiv : int, optional + Number of divisions. Default is 1. + + Returns + ------- + w : ndarray + Weights for an Np-point central derivative. Its size is `Np`. + + Notes + ----- + Can be inaccurate for a large number of points. + + Examples + -------- + We can calculate a derivative value of a function. + + >>> def f(x): + ... return 2 * x**2 + 3 + >>> x = 3.0 # derivative point + >>> h = 0.1 # differential step + >>> Np = 3 # point number for central derivative + >>> weights = _central_diff_weights(Np) # weights for first derivative + >>> vals = [f(x + (i - Np/2) * h) for i in range(Np)] + >>> sum(w * v for (w, v) in zip(weights, vals))/h + 11.79999999999998 + + This value is close to the analytical solution: + f'(x) = 4x, so f'(3) = 12 + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Finite_difference + + """ + if Np < ndiv + 1: + raise ValueError( + "Number of points must be at least the derivative order + 1." + ) + if Np % 2 == 0: + raise ValueError("The number of points must be odd.") + from scipy import linalg + + ho = Np >> 1 + x = arange(-ho, ho + 1.0) + x = x[:, newaxis] + X = x**0.0 + for k in range(1, Np): + X = hstack([X, x**k]) + w = prod(arange(1, ndiv + 1), axis=0) * linalg.inv(X)[ndiv] + return w + + +def _derivative(func, x0, dx=1.0, n=1, args=(), order=3): + """ + Find the nth derivative of a function at a point. + + Given a function, use a central difference formula with spacing `dx` to + compute the nth derivative at `x0`. + + Parameters + ---------- + func : function + Input function. + x0 : float + The point at which the nth derivative is found. + dx : float, optional + Spacing. + n : int, optional + Order of the derivative. Default is 1. + args : tuple, optional + Arguments + order : int, optional + Number of points to use, must be odd. + + Notes + ----- + Decreasing the step size too small can result in round-off error. + + Examples + -------- + >>> def f(x): + ... return x**3 + x**2 + >>> _derivative(f, 1.0, dx=1e-6) + 4.9999999999217337 + + """ + if order < n + 1: + raise ValueError( + "'order' (the number of points used to compute the derivative), " + "must be at least the derivative order 'n' + 1." + ) + if order % 2 == 0: + raise ValueError( + "'order' (the number of points used to compute the derivative) " + "must be odd." + ) + # pre-computed for n=1 and 2 and low-order for speed. + if n == 1: + if order == 3: + weights = array([-1, 0, 1]) / 2.0 + elif order == 5: + weights = array([1, -8, 0, 8, -1]) / 12.0 + elif order == 7: + weights = array([-1, 9, -45, 0, 45, -9, 1]) / 60.0 + elif order == 9: + weights = array([3, -32, 168, -672, 0, 672, -168, 32, -3]) / 840.0 + else: + weights = _central_diff_weights(order, 1) + elif n == 2: + if order == 3: + weights = array([1, -2.0, 1]) + elif order == 5: + weights = array([-1, 16, -30, 16, -1]) / 12.0 + elif order == 7: + weights = array([2, -27, 270, -490, 270, -27, 2]) / 180.0 + elif order == 9: + weights = ( + array([-9, 128, -1008, 8064, -14350, 8064, -1008, 128, -9]) + / 5040.0 + ) + else: + weights = _central_diff_weights(order, 2) + else: + weights = _central_diff_weights(order, n) + val = 0.0 + ho = order >> 1 + for k in range(order): + val += weights[k] * func(x0 + (k - ho) * dx, *args) + return val / prod((dx,) * n, axis=0) diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_fpumode.cpython-310-x86_64-linux-gnu.so b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_fpumode.cpython-310-x86_64-linux-gnu.so new file mode 100644 index 0000000000000000000000000000000000000000..3a443899bde3481ed6c1359eff4ef9696f6c8e4d Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_fpumode.cpython-310-x86_64-linux-gnu.so differ diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_gcutils.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_gcutils.py new file mode 100644 index 0000000000000000000000000000000000000000..854ae36228614f3eb8849e9f95abf0dd387b5d35 --- /dev/null +++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_gcutils.py @@ -0,0 +1,105 @@ +""" +Module for testing automatic garbage collection of objects + +.. autosummary:: + :toctree: generated/ + + set_gc_state - enable or disable garbage collection + gc_state - context manager for given state of garbage collector + assert_deallocated - context manager to check for circular references on object + +""" +import weakref +import gc + +from contextlib import contextmanager +from platform import python_implementation + +__all__ = ['set_gc_state', 'gc_state', 'assert_deallocated'] + + +IS_PYPY = python_implementation() == 'PyPy' + + +class ReferenceError(AssertionError): + pass + + +def set_gc_state(state): + """ Set status of garbage collector """ + if gc.isenabled() == state: + return + if state: + gc.enable() + else: + gc.disable() + + +@contextmanager +def gc_state(state): + """ Context manager to set state of garbage collector to `state` + + Parameters + ---------- + state : bool + True for gc enabled, False for disabled + + Examples + -------- + >>> with gc_state(False): + ... assert not gc.isenabled() + >>> with gc_state(True): + ... assert gc.isenabled() + """ + orig_state = gc.isenabled() + set_gc_state(state) + yield + set_gc_state(orig_state) + + +@contextmanager +def assert_deallocated(func, *args, **kwargs): + """Context manager to check that object is deallocated + + This is useful for checking that an object can be freed directly by + reference counting, without requiring gc to break reference cycles. + GC is disabled inside the context manager. + + This check is not available on PyPy. + + Parameters + ---------- + func : callable + Callable to create object to check + \\*args : sequence + positional arguments to `func` in order to create object to check + \\*\\*kwargs : dict + keyword arguments to `func` in order to create object to check + + Examples + -------- + >>> class C: pass + >>> with assert_deallocated(C) as c: + ... # do something + ... del c + + >>> class C: + ... def __init__(self): + ... self._circular = self # Make circular reference + >>> with assert_deallocated(C) as c: #doctest: +IGNORE_EXCEPTION_DETAIL + ... # do something + ... del c + Traceback (most recent call last): + ... + ReferenceError: Remaining reference(s) to object + """ + if IS_PYPY: + raise RuntimeError("assert_deallocated is unavailable on PyPy") + + with gc_state(False): + obj = func(*args, **kwargs) + ref = weakref.ref(obj) + yield obj + del obj + if ref() is not None: + raise ReferenceError("Remaining reference(s) to object") diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_pep440.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_pep440.py new file mode 100644 index 0000000000000000000000000000000000000000..d546e32a0349461a0aab76bfb4636ebf25227ca0 --- /dev/null +++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_pep440.py @@ -0,0 +1,487 @@ +"""Utility to compare pep440 compatible version strings. + +The LooseVersion and StrictVersion classes that distutils provides don't +work; they don't recognize anything like alpha/beta/rc/dev versions. +""" + +# Copyright (c) Donald Stufft and individual contributors. +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import collections +import itertools +import re + + +__all__ = [ + "parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN", +] + + +# BEGIN packaging/_structures.py + + +class Infinity: + def __repr__(self): + return "Infinity" + + def __hash__(self): + return hash(repr(self)) + + def __lt__(self, other): + return False + + def __le__(self, other): + return False + + def __eq__(self, other): + return isinstance(other, self.__class__) + + def __ne__(self, other): + return not isinstance(other, self.__class__) + + def __gt__(self, other): + return True + + def __ge__(self, other): + return True + + def __neg__(self): + return NegativeInfinity + + +Infinity = Infinity() + + +class NegativeInfinity: + def __repr__(self): + return "-Infinity" + + def __hash__(self): + return hash(repr(self)) + + def __lt__(self, other): + return True + + def __le__(self, other): + return True + + def __eq__(self, other): + return isinstance(other, self.__class__) + + def __ne__(self, other): + return not isinstance(other, self.__class__) + + def __gt__(self, other): + return False + + def __ge__(self, other): + return False + + def __neg__(self): + return Infinity + + +# BEGIN packaging/version.py + + +NegativeInfinity = NegativeInfinity() + +_Version = collections.namedtuple( + "_Version", + ["epoch", "release", "dev", "pre", "post", "local"], +) + + +def parse(version): + """ + Parse the given version string and return either a :class:`Version` object + or a :class:`LegacyVersion` object depending on if the given version is + a valid PEP 440 version or a legacy version. + """ + try: + return Version(version) + except InvalidVersion: + return LegacyVersion(version) + + +class InvalidVersion(ValueError): + """ + An invalid version was found, users should refer to PEP 440. + """ + + +class _BaseVersion: + + def __hash__(self): + return hash(self._key) + + def __lt__(self, other): + return self._compare(other, lambda s, o: s < o) + + def __le__(self, other): + return self._compare(other, lambda s, o: s <= o) + + def __eq__(self, other): + return self._compare(other, lambda s, o: s == o) + + def __ge__(self, other): + return self._compare(other, lambda s, o: s >= o) + + def __gt__(self, other): + return self._compare(other, lambda s, o: s > o) + + def __ne__(self, other): + return self._compare(other, lambda s, o: s != o) + + def _compare(self, other, method): + if not isinstance(other, _BaseVersion): + return NotImplemented + + return method(self._key, other._key) + + +class LegacyVersion(_BaseVersion): + + def __init__(self, version): + self._version = str(version) + self._key = _legacy_cmpkey(self._version) + + def __str__(self): + return self._version + + def __repr__(self): + return f"" + + @property + def public(self): + return self._version + + @property + def base_version(self): + return self._version + + @property + def local(self): + return None + + @property + def is_prerelease(self): + return False + + @property + def is_postrelease(self): + return False + + +_legacy_version_component_re = re.compile( + r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE, +) + +_legacy_version_replacement_map = { + "pre": "c", "preview": "c", "-": "final-", "rc": "c", "dev": "@", +} + + +def _parse_version_parts(s): + for part in _legacy_version_component_re.split(s): + part = _legacy_version_replacement_map.get(part, part) + + if not part or part == ".": + continue + + if part[:1] in "0123456789": + # pad for numeric comparison + yield part.zfill(8) + else: + yield "*" + part + + # ensure that alpha/beta/candidate are before final + yield "*final" + + +def _legacy_cmpkey(version): + # We hardcode an epoch of -1 here. A PEP 440 version can only have an epoch + # greater than or equal to 0. This will effectively put the LegacyVersion, + # which uses the defacto standard originally implemented by setuptools, + # as before all PEP 440 versions. + epoch = -1 + + # This scheme is taken from pkg_resources.parse_version setuptools prior to + # its adoption of the packaging library. + parts = [] + for part in _parse_version_parts(version.lower()): + if part.startswith("*"): + # remove "-" before a prerelease tag + if part < "*final": + while parts and parts[-1] == "*final-": + parts.pop() + + # remove trailing zeros from each series of numeric parts + while parts and parts[-1] == "00000000": + parts.pop() + + parts.append(part) + parts = tuple(parts) + + return epoch, parts + + +# Deliberately not anchored to the start and end of the string, to make it +# easier for 3rd party code to reuse +VERSION_PATTERN = r""" + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+            [-_\.]?
+            (?P(a|b|c|rc|alpha|beta|pre|preview))
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+        (?P                                         # post release
+            (?:-(?P[0-9]+))
+            |
+            (?:
+                [-_\.]?
+                (?Ppost|rev|r)
+                [-_\.]?
+                (?P[0-9]+)?
+            )
+        )?
+        (?P                                          # dev release
+            [-_\.]?
+            (?Pdev)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+    )
+    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+"""
+
+
+class Version(_BaseVersion):
+
+    _regex = re.compile(
+        r"^\s*" + VERSION_PATTERN + r"\s*$",
+        re.VERBOSE | re.IGNORECASE,
+    )
+
+    def __init__(self, version):
+        # Validate the version and parse it into pieces
+        match = self._regex.search(version)
+        if not match:
+            raise InvalidVersion(f"Invalid version: '{version}'")
+
+        # Store the parsed out pieces of the version
+        self._version = _Version(
+            epoch=int(match.group("epoch")) if match.group("epoch") else 0,
+            release=tuple(int(i) for i in match.group("release").split(".")),
+            pre=_parse_letter_version(
+                match.group("pre_l"),
+                match.group("pre_n"),
+            ),
+            post=_parse_letter_version(
+                match.group("post_l"),
+                match.group("post_n1") or match.group("post_n2"),
+            ),
+            dev=_parse_letter_version(
+                match.group("dev_l"),
+                match.group("dev_n"),
+            ),
+            local=_parse_local_version(match.group("local")),
+        )
+
+        # Generate a key which will be used for sorting
+        self._key = _cmpkey(
+            self._version.epoch,
+            self._version.release,
+            self._version.pre,
+            self._version.post,
+            self._version.dev,
+            self._version.local,
+        )
+
+    def __repr__(self):
+        return f""
+
+    def __str__(self):
+        parts = []
+
+        # Epoch
+        if self._version.epoch != 0:
+            parts.append(f"{self._version.epoch}!")
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self._version.release))
+
+        # Pre-release
+        if self._version.pre is not None:
+            parts.append("".join(str(x) for x in self._version.pre))
+
+        # Post-release
+        if self._version.post is not None:
+            parts.append(f".post{self._version.post[1]}")
+
+        # Development release
+        if self._version.dev is not None:
+            parts.append(f".dev{self._version.dev[1]}")
+
+        # Local version segment
+        if self._version.local is not None:
+            parts.append(
+                "+{}".format(".".join(str(x) for x in self._version.local))
+            )
+
+        return "".join(parts)
+
+    @property
+    def public(self):
+        return str(self).split("+", 1)[0]
+
+    @property
+    def base_version(self):
+        parts = []
+
+        # Epoch
+        if self._version.epoch != 0:
+            parts.append(f"{self._version.epoch}!")
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self._version.release))
+
+        return "".join(parts)
+
+    @property
+    def local(self):
+        version_string = str(self)
+        if "+" in version_string:
+            return version_string.split("+", 1)[1]
+
+    @property
+    def is_prerelease(self):
+        return bool(self._version.dev or self._version.pre)
+
+    @property
+    def is_postrelease(self):
+        return bool(self._version.post)
+
+
+def _parse_letter_version(letter, number):
+    if letter:
+        # We assume there is an implicit 0 in a pre-release if there is
+        # no numeral associated with it.
+        if number is None:
+            number = 0
+
+        # We normalize any letters to their lower-case form
+        letter = letter.lower()
+
+        # We consider some words to be alternate spellings of other words and
+        # in those cases we want to normalize the spellings to our preferred
+        # spelling.
+        if letter == "alpha":
+            letter = "a"
+        elif letter == "beta":
+            letter = "b"
+        elif letter in ["c", "pre", "preview"]:
+            letter = "rc"
+        elif letter in ["rev", "r"]:
+            letter = "post"
+
+        return letter, int(number)
+    if not letter and number:
+        # We assume that if we are given a number but not given a letter,
+        # then this is using the implicit post release syntax (e.g., 1.0-1)
+        letter = "post"
+
+        return letter, int(number)
+
+
+_local_version_seperators = re.compile(r"[\._-]")
+
+
+def _parse_local_version(local):
+    """
+    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
+    """
+    if local is not None:
+        return tuple(
+            part.lower() if not part.isdigit() else int(part)
+            for part in _local_version_seperators.split(local)
+        )
+
+
+def _cmpkey(epoch, release, pre, post, dev, local):
+    # When we compare a release version, we want to compare it with all of the
+    # trailing zeros removed. So we'll use a reverse the list, drop all the now
+    # leading zeros until we come to something non-zero, then take the rest,
+    # re-reverse it back into the correct order, and make it a tuple and use
+    # that for our sorting key.
+    release = tuple(
+        reversed(list(
+            itertools.dropwhile(
+                lambda x: x == 0,
+                reversed(release),
+            )
+        ))
+    )
+
+    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
+    # We'll do this by abusing the pre-segment, but we _only_ want to do this
+    # if there is no pre- or a post-segment. If we have one of those, then
+    # the normal sorting rules will handle this case correctly.
+    if pre is None and post is None and dev is not None:
+        pre = -Infinity
+    # Versions without a pre-release (except as noted above) should sort after
+    # those with one.
+    elif pre is None:
+        pre = Infinity
+
+    # Versions without a post-segment should sort before those with one.
+    if post is None:
+        post = -Infinity
+
+    # Versions without a development segment should sort after those with one.
+    if dev is None:
+        dev = Infinity
+
+    if local is None:
+        # Versions without a local segment should sort before those with one.
+        local = -Infinity
+    else:
+        # Versions with a local segment need that segment parsed to implement
+        # the sorting rules in PEP440.
+        # - Alphanumeric segments sort before numeric segments
+        # - Alphanumeric segments sort lexicographically
+        # - Numeric segments sort numerically
+        # - Shorter versions sort before longer versions when the prefixes
+        #   match exactly
+        local = tuple(
+            (i, "") if isinstance(i, int) else (-Infinity, i)
+            for i in local
+        )
+
+    return epoch, release, pre, post, dev, local
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_test_ccallback.cpython-310-x86_64-linux-gnu.so b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_test_ccallback.cpython-310-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..bfb217d3ba8618170d57b5451f369660eb4ede64
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_test_ccallback.cpython-310-x86_64-linux-gnu.so differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_test_deprecation_call.cpython-310-x86_64-linux-gnu.so b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_test_deprecation_call.cpython-310-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..18e11f349a5c869c5e27e41dafa35145f6a5fae8
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_test_deprecation_call.cpython-310-x86_64-linux-gnu.so differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_test_deprecation_def.cpython-310-x86_64-linux-gnu.so b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_test_deprecation_def.cpython-310-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..4e147503985965d529df2f379bd8b095c203079e
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_test_deprecation_def.cpython-310-x86_64-linux-gnu.so differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_testutils.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_testutils.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a830edcdf0cb19d68eb914c98c3b1dcfafab823
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_testutils.py
@@ -0,0 +1,337 @@
+"""
+Generic test utilities.
+
+"""
+
+import inspect
+import os
+import re
+import shutil
+import subprocess
+import sys
+import sysconfig
+from importlib.util import module_from_spec, spec_from_file_location
+
+import numpy as np
+import scipy
+
+try:
+    # Need type: ignore[import-untyped] for mypy >= 1.6
+    import cython  # type: ignore[import-untyped]
+    from Cython.Compiler.Version import (  # type: ignore[import-untyped]
+        version as cython_version,
+    )
+except ImportError:
+    cython = None
+else:
+    from scipy._lib import _pep440
+    required_version = '3.0.8'
+    if _pep440.parse(cython_version) < _pep440.Version(required_version):
+        # too old or wrong cython, skip Cython API tests
+        cython = None
+
+
+__all__ = ['PytestTester', 'check_free_memory', '_TestPythranFunc', 'IS_MUSL']
+
+
+IS_MUSL = False
+# alternate way is
+# from packaging.tags import sys_tags
+#     _tags = list(sys_tags())
+#     if 'musllinux' in _tags[0].platform:
+_v = sysconfig.get_config_var('HOST_GNU_TYPE') or ''
+if 'musl' in _v:
+    IS_MUSL = True
+
+
+IS_EDITABLE = 'editable' in scipy.__path__[0]
+
+
+class FPUModeChangeWarning(RuntimeWarning):
+    """Warning about FPU mode change"""
+    pass
+
+
+class PytestTester:
+    """
+    Run tests for this namespace
+
+    ``scipy.test()`` runs tests for all of SciPy, with the default settings.
+    When used from a submodule (e.g., ``scipy.cluster.test()``, only the tests
+    for that namespace are run.
+
+    Parameters
+    ----------
+    label : {'fast', 'full'}, optional
+        Whether to run only the fast tests, or also those marked as slow.
+        Default is 'fast'.
+    verbose : int, optional
+        Test output verbosity. Default is 1.
+    extra_argv : list, optional
+        Arguments to pass through to Pytest.
+    doctests : bool, optional
+        Whether to run doctests or not. Default is False.
+    coverage : bool, optional
+        Whether to run tests with code coverage measurements enabled.
+        Default is False.
+    tests : list of str, optional
+        List of module names to run tests for. By default, uses the module
+        from which the ``test`` function is called.
+    parallel : int, optional
+        Run tests in parallel with pytest-xdist, if number given is larger than
+        1. Default is 1.
+
+    """
+    def __init__(self, module_name):
+        self.module_name = module_name
+
+    def __call__(self, label="fast", verbose=1, extra_argv=None, doctests=False,
+                 coverage=False, tests=None, parallel=None):
+        import pytest
+
+        module = sys.modules[self.module_name]
+        module_path = os.path.abspath(module.__path__[0])
+
+        pytest_args = ['--showlocals', '--tb=short']
+
+        if doctests:
+            pytest_args += [
+                "--doctest-modules",
+                "--ignore=scipy/interpolate/_interpnd_info.py",
+                "--ignore=scipy/_lib/array_api_compat",
+                "--ignore=scipy/_lib/highs",
+                "--ignore=scipy/_lib/unuran",
+                "--ignore=scipy/_lib/_gcutils.py",
+                "--ignore=scipy/_lib/doccer.py",
+                "--ignore=scipy/_lib/_uarray",
+            ]
+
+        if extra_argv:
+            pytest_args += list(extra_argv)
+
+        if verbose and int(verbose) > 1:
+            pytest_args += ["-" + "v"*(int(verbose)-1)]
+
+        if coverage:
+            pytest_args += ["--cov=" + module_path]
+
+        if label == "fast":
+            pytest_args += ["-m", "not slow"]
+        elif label != "full":
+            pytest_args += ["-m", label]
+
+        if tests is None:
+            tests = [self.module_name]
+
+        if parallel is not None and parallel > 1:
+            if _pytest_has_xdist():
+                pytest_args += ['-n', str(parallel)]
+            else:
+                import warnings
+                warnings.warn('Could not run tests in parallel because '
+                              'pytest-xdist plugin is not available.',
+                              stacklevel=2)
+
+        pytest_args += ['--pyargs'] + list(tests)
+
+        try:
+            code = pytest.main(pytest_args)
+        except SystemExit as exc:
+            code = exc.code
+
+        return (code == 0)
+
+
+class _TestPythranFunc:
+    '''
+    These are situations that can be tested in our pythran tests:
+    - A function with multiple array arguments and then
+      other positional and keyword arguments.
+    - A function with array-like keywords (e.g. `def somefunc(x0, x1=None)`.
+    Note: list/tuple input is not yet tested!
+
+    `self.arguments`: A dictionary which key is the index of the argument,
+                      value is tuple(array value, all supported dtypes)
+    `self.partialfunc`: A function used to freeze some non-array argument
+                        that of no interests in the original function
+    '''
+    ALL_INTEGER = [np.int8, np.int16, np.int32, np.int64, np.intc, np.intp]
+    ALL_FLOAT = [np.float32, np.float64]
+    ALL_COMPLEX = [np.complex64, np.complex128]
+
+    def setup_method(self):
+        self.arguments = {}
+        self.partialfunc = None
+        self.expected = None
+
+    def get_optional_args(self, func):
+        # get optional arguments with its default value,
+        # used for testing keywords
+        signature = inspect.signature(func)
+        optional_args = {}
+        for k, v in signature.parameters.items():
+            if v.default is not inspect.Parameter.empty:
+                optional_args[k] = v.default
+        return optional_args
+
+    def get_max_dtype_list_length(self):
+        # get the max supported dtypes list length in all arguments
+        max_len = 0
+        for arg_idx in self.arguments:
+            cur_len = len(self.arguments[arg_idx][1])
+            if cur_len > max_len:
+                max_len = cur_len
+        return max_len
+
+    def get_dtype(self, dtype_list, dtype_idx):
+        # get the dtype from dtype_list via index
+        # if the index is out of range, then return the last dtype
+        if dtype_idx > len(dtype_list)-1:
+            return dtype_list[-1]
+        else:
+            return dtype_list[dtype_idx]
+
+    def test_all_dtypes(self):
+        for type_idx in range(self.get_max_dtype_list_length()):
+            args_array = []
+            for arg_idx in self.arguments:
+                new_dtype = self.get_dtype(self.arguments[arg_idx][1],
+                                           type_idx)
+                args_array.append(self.arguments[arg_idx][0].astype(new_dtype))
+            self.pythranfunc(*args_array)
+
+    def test_views(self):
+        args_array = []
+        for arg_idx in self.arguments:
+            args_array.append(self.arguments[arg_idx][0][::-1][::-1])
+        self.pythranfunc(*args_array)
+
+    def test_strided(self):
+        args_array = []
+        for arg_idx in self.arguments:
+            args_array.append(np.repeat(self.arguments[arg_idx][0],
+                                        2, axis=0)[::2])
+        self.pythranfunc(*args_array)
+
+
+def _pytest_has_xdist():
+    """
+    Check if the pytest-xdist plugin is installed, providing parallel tests
+    """
+    # Check xdist exists without importing, otherwise pytests emits warnings
+    from importlib.util import find_spec
+    return find_spec('xdist') is not None
+
+
+def check_free_memory(free_mb):
+    """
+    Check *free_mb* of memory is available, otherwise do pytest.skip
+    """
+    import pytest
+
+    try:
+        mem_free = _parse_size(os.environ['SCIPY_AVAILABLE_MEM'])
+        msg = '{} MB memory required, but environment SCIPY_AVAILABLE_MEM={}'.format(
+            free_mb, os.environ['SCIPY_AVAILABLE_MEM'])
+    except KeyError:
+        mem_free = _get_mem_available()
+        if mem_free is None:
+            pytest.skip("Could not determine available memory; set SCIPY_AVAILABLE_MEM "
+                        "variable to free memory in MB to run the test.")
+        msg = f'{free_mb} MB memory required, but {mem_free/1e6} MB available'
+
+    if mem_free < free_mb * 1e6:
+        pytest.skip(msg)
+
+
+def _parse_size(size_str):
+    suffixes = {'': 1e6,
+                'b': 1.0,
+                'k': 1e3, 'M': 1e6, 'G': 1e9, 'T': 1e12,
+                'kb': 1e3, 'Mb': 1e6, 'Gb': 1e9, 'Tb': 1e12,
+                'kib': 1024.0, 'Mib': 1024.0**2, 'Gib': 1024.0**3, 'Tib': 1024.0**4}
+    m = re.match(r'^\s*(\d+)\s*({})\s*$'.format('|'.join(suffixes.keys())),
+                 size_str,
+                 re.I)
+    if not m or m.group(2) not in suffixes:
+        raise ValueError("Invalid size string")
+
+    return float(m.group(1)) * suffixes[m.group(2)]
+
+
+def _get_mem_available():
+    """
+    Get information about memory available, not counting swap.
+    """
+    try:
+        import psutil
+        return psutil.virtual_memory().available
+    except (ImportError, AttributeError):
+        pass
+
+    if sys.platform.startswith('linux'):
+        info = {}
+        with open('/proc/meminfo') as f:
+            for line in f:
+                p = line.split()
+                info[p[0].strip(':').lower()] = float(p[1]) * 1e3
+
+        if 'memavailable' in info:
+            # Linux >= 3.14
+            return info['memavailable']
+        else:
+            return info['memfree'] + info['cached']
+
+    return None
+
+def _test_cython_extension(tmp_path, srcdir):
+    """
+    Helper function to test building and importing Cython modules that
+    make use of the Cython APIs for BLAS, LAPACK, optimize, and special.
+    """
+    import pytest
+    try:
+        subprocess.check_call(["meson", "--version"])
+    except FileNotFoundError:
+        pytest.skip("No usable 'meson' found")
+
+    # build the examples in a temporary directory
+    mod_name = os.path.split(srcdir)[1]
+    shutil.copytree(srcdir, tmp_path / mod_name)
+    build_dir = tmp_path / mod_name / 'tests' / '_cython_examples'
+    target_dir = build_dir / 'build'
+    os.makedirs(target_dir, exist_ok=True)
+
+    # Ensure we use the correct Python interpreter even when `meson` is
+    # installed in a different Python environment (see numpy#24956)
+    native_file = str(build_dir / 'interpreter-native-file.ini')
+    with open(native_file, 'w') as f:
+        f.write("[binaries]\n")
+        f.write(f"python = '{sys.executable}'")
+
+    if sys.platform == "win32":
+        subprocess.check_call(["meson", "setup",
+                               "--buildtype=release",
+                               "--native-file", native_file,
+                               "--vsenv", str(build_dir)],
+                              cwd=target_dir,
+                              )
+    else:
+        subprocess.check_call(["meson", "setup",
+                               "--native-file", native_file, str(build_dir)],
+                              cwd=target_dir
+                              )
+    subprocess.check_call(["meson", "compile", "-vv"], cwd=target_dir)
+
+    # import without adding the directory to sys.path
+    suffix = sysconfig.get_config_var('EXT_SUFFIX')
+
+    def load(modname):
+        so = (target_dir / modname).with_suffix(suffix)
+        spec = spec_from_file_location(modname, so)
+        mod = module_from_spec(spec)
+        spec.loader.exec_module(mod)
+        return mod
+
+    # test that the module can be imported
+    return load("extending"), load("extending_cpp")
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_threadsafety.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_threadsafety.py
new file mode 100644
index 0000000000000000000000000000000000000000..feea0c5923903b0b751e66bdf192e7f1d2b7ac67
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_threadsafety.py
@@ -0,0 +1,58 @@
+import threading
+
+import scipy._lib.decorator
+
+
+__all__ = ['ReentrancyError', 'ReentrancyLock', 'non_reentrant']
+
+
+class ReentrancyError(RuntimeError):
+    pass
+
+
+class ReentrancyLock:
+    """
+    Threading lock that raises an exception for reentrant calls.
+
+    Calls from different threads are serialized, and nested calls from the
+    same thread result to an error.
+
+    The object can be used as a context manager or to decorate functions
+    via the decorate() method.
+
+    """
+
+    def __init__(self, err_msg):
+        self._rlock = threading.RLock()
+        self._entered = False
+        self._err_msg = err_msg
+
+    def __enter__(self):
+        self._rlock.acquire()
+        if self._entered:
+            self._rlock.release()
+            raise ReentrancyError(self._err_msg)
+        self._entered = True
+
+    def __exit__(self, type, value, traceback):
+        self._entered = False
+        self._rlock.release()
+
+    def decorate(self, func):
+        def caller(func, *a, **kw):
+            with self:
+                return func(*a, **kw)
+        return scipy._lib.decorator.decorate(func, caller)
+
+
+def non_reentrant(err_msg=None):
+    """
+    Decorate a function with a threading lock and prevent reentrant calls.
+    """
+    def decorator(func):
+        msg = err_msg
+        if msg is None:
+            msg = "%s is not re-entrant" % func.__name__
+        lock = ReentrancyLock(msg)
+        return lock.decorate(func)
+    return decorator
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_tmpdirs.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_tmpdirs.py
new file mode 100644
index 0000000000000000000000000000000000000000..0f9fd546a9d2ae3e9a20c0684f79eb0b3d61ee92
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_tmpdirs.py
@@ -0,0 +1,86 @@
+''' Contexts for *with* statement providing temporary directories
+'''
+import os
+from contextlib import contextmanager
+from shutil import rmtree
+from tempfile import mkdtemp
+
+
+@contextmanager
+def tempdir():
+    """Create and return a temporary directory. This has the same
+    behavior as mkdtemp but can be used as a context manager.
+
+    Upon exiting the context, the directory and everything contained
+    in it are removed.
+
+    Examples
+    --------
+    >>> import os
+    >>> with tempdir() as tmpdir:
+    ...     fname = os.path.join(tmpdir, 'example_file.txt')
+    ...     with open(fname, 'wt') as fobj:
+    ...         _ = fobj.write('a string\\n')
+    >>> os.path.exists(tmpdir)
+    False
+    """
+    d = mkdtemp()
+    yield d
+    rmtree(d)
+
+
+@contextmanager
+def in_tempdir():
+    ''' Create, return, and change directory to a temporary directory
+
+    Examples
+    --------
+    >>> import os
+    >>> my_cwd = os.getcwd()
+    >>> with in_tempdir() as tmpdir:
+    ...     _ = open('test.txt', 'wt').write('some text')
+    ...     assert os.path.isfile('test.txt')
+    ...     assert os.path.isfile(os.path.join(tmpdir, 'test.txt'))
+    >>> os.path.exists(tmpdir)
+    False
+    >>> os.getcwd() == my_cwd
+    True
+    '''
+    pwd = os.getcwd()
+    d = mkdtemp()
+    os.chdir(d)
+    yield d
+    os.chdir(pwd)
+    rmtree(d)
+
+
+@contextmanager
+def in_dir(dir=None):
+    """ Change directory to given directory for duration of ``with`` block
+
+    Useful when you want to use `in_tempdir` for the final test, but
+    you are still debugging. For example, you may want to do this in the end:
+
+    >>> with in_tempdir() as tmpdir:
+    ...     # do something complicated which might break
+    ...     pass
+
+    But, indeed, the complicated thing does break, and meanwhile, the
+    ``in_tempdir`` context manager wiped out the directory with the
+    temporary files that you wanted for debugging. So, while debugging, you
+    replace with something like:
+
+    >>> with in_dir() as tmpdir: # Use working directory by default
+    ...     # do something complicated which might break
+    ...     pass
+
+    You can then look at the temporary file outputs to debug what is happening,
+    fix, and finally replace ``in_dir`` with ``in_tempdir`` again.
+    """
+    cwd = os.getcwd()
+    if dir is None:
+        yield cwd
+        return
+    os.chdir(dir)
+    yield dir
+    os.chdir(cwd)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_util.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_util.py
new file mode 100644
index 0000000000000000000000000000000000000000..b59677b954fc386e7b531d0dd7b9f64c852012b6
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/_util.py
@@ -0,0 +1,954 @@
+import re
+from contextlib import contextmanager
+import functools
+import operator
+import warnings
+import numbers
+from collections import namedtuple
+import inspect
+import math
+from typing import (
+    Optional,
+    Union,
+    TYPE_CHECKING,
+    TypeVar,
+)
+
+import numpy as np
+from scipy._lib._array_api import array_namespace, is_numpy, size as xp_size
+
+
+AxisError: type[Exception]
+ComplexWarning: type[Warning]
+VisibleDeprecationWarning: type[Warning]
+
+if np.lib.NumpyVersion(np.__version__) >= '1.25.0':
+    from numpy.exceptions import (
+        AxisError, ComplexWarning, VisibleDeprecationWarning,
+        DTypePromotionError
+    )
+else:
+    from numpy import (  # type: ignore[attr-defined, no-redef]
+        AxisError, ComplexWarning, VisibleDeprecationWarning  # noqa: F401
+    )
+    DTypePromotionError = TypeError  # type: ignore
+
+np_long: type
+np_ulong: type
+
+if np.lib.NumpyVersion(np.__version__) >= "2.0.0.dev0":
+    try:
+        with warnings.catch_warnings():
+            warnings.filterwarnings(
+                "ignore",
+                r".*In the future `np\.long` will be defined as.*",
+                FutureWarning,
+            )
+            np_long = np.long  # type: ignore[attr-defined]
+            np_ulong = np.ulong  # type: ignore[attr-defined]
+    except AttributeError:
+            np_long = np.int_
+            np_ulong = np.uint
+else:
+    np_long = np.int_
+    np_ulong = np.uint
+
+IntNumber = Union[int, np.integer]
+DecimalNumber = Union[float, np.floating, np.integer]
+
+copy_if_needed: Optional[bool]
+
+if np.lib.NumpyVersion(np.__version__) >= "2.0.0":
+    copy_if_needed = None
+elif np.lib.NumpyVersion(np.__version__) < "1.28.0":
+    copy_if_needed = False
+else:
+    # 2.0.0 dev versions, handle cases where copy may or may not exist
+    try:
+        np.array([1]).__array__(copy=None)  # type: ignore[call-overload]
+        copy_if_needed = None
+    except TypeError:
+        copy_if_needed = False
+
+# Since Generator was introduced in numpy 1.17, the following condition is needed for
+# backward compatibility
+if TYPE_CHECKING:
+    SeedType = Optional[Union[IntNumber, np.random.Generator,
+                              np.random.RandomState]]
+    GeneratorType = TypeVar("GeneratorType", bound=Union[np.random.Generator,
+                                                         np.random.RandomState])
+
+try:
+    from numpy.random import Generator as Generator
+except ImportError:
+    class Generator:  # type: ignore[no-redef]
+        pass
+
+
+def _lazywhere(cond, arrays, f, fillvalue=None, f2=None):
+    """Return elements chosen from two possibilities depending on a condition
+
+    Equivalent to ``f(*arrays) if cond else fillvalue`` performed elementwise.
+
+    Parameters
+    ----------
+    cond : array
+        The condition (expressed as a boolean array).
+    arrays : tuple of array
+        Arguments to `f` (and `f2`). Must be broadcastable with `cond`.
+    f : callable
+        Where `cond` is True, output will be ``f(arr1[cond], arr2[cond], ...)``
+    fillvalue : object
+        If provided, value with which to fill output array where `cond` is
+        not True.
+    f2 : callable
+        If provided, output will be ``f2(arr1[cond], arr2[cond], ...)`` where
+        `cond` is not True.
+
+    Returns
+    -------
+    out : array
+        An array with elements from the output of `f` where `cond` is True
+        and `fillvalue` (or elements from the output of `f2`) elsewhere. The
+        returned array has data type determined by Type Promotion Rules
+        with the output of `f` and `fillvalue` (or the output of `f2`).
+
+    Notes
+    -----
+    ``xp.where(cond, x, fillvalue)`` requires explicitly forming `x` even where
+    `cond` is False. This function evaluates ``f(arr1[cond], arr2[cond], ...)``
+    onle where `cond` ``is True.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> a, b = np.array([1, 2, 3, 4]), np.array([5, 6, 7, 8])
+    >>> def f(a, b):
+    ...     return a*b
+    >>> _lazywhere(a > 2, (a, b), f, np.nan)
+    array([ nan,  nan,  21.,  32.])
+
+    """
+    xp = array_namespace(cond, *arrays)
+
+    if (f2 is fillvalue is None) or (f2 is not None and fillvalue is not None):
+        raise ValueError("Exactly one of `fillvalue` or `f2` must be given.")
+
+    args = xp.broadcast_arrays(cond, *arrays)
+    bool_dtype = xp.asarray([True]).dtype  # numpy 1.xx doesn't have `bool`
+    cond, arrays = xp.astype(args[0], bool_dtype, copy=False), args[1:]
+
+    temp1 = xp.asarray(f(*(arr[cond] for arr in arrays)))
+
+    if f2 is None:
+        fillvalue = xp.asarray(fillvalue)
+        dtype = xp.result_type(temp1.dtype, fillvalue.dtype)
+        out = xp.full(cond.shape, fill_value=fillvalue, dtype=dtype)
+    else:
+        ncond = ~cond
+        temp2 = xp.asarray(f2(*(arr[ncond] for arr in arrays)))
+        dtype = xp.result_type(temp1, temp2)
+        out = xp.empty(cond.shape, dtype=dtype)
+        out[ncond] = temp2
+
+    out[cond] = temp1
+
+    return out
+
+
+def _lazyselect(condlist, choicelist, arrays, default=0):
+    """
+    Mimic `np.select(condlist, choicelist)`.
+
+    Notice, it assumes that all `arrays` are of the same shape or can be
+    broadcasted together.
+
+    All functions in `choicelist` must accept array arguments in the order
+    given in `arrays` and must return an array of the same shape as broadcasted
+    `arrays`.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> x = np.arange(6)
+    >>> np.select([x <3, x > 3], [x**2, x**3], default=0)
+    array([  0,   1,   4,   0,  64, 125])
+
+    >>> _lazyselect([x < 3, x > 3], [lambda x: x**2, lambda x: x**3], (x,))
+    array([   0.,    1.,    4.,   0.,   64.,  125.])
+
+    >>> a = -np.ones_like(x)
+    >>> _lazyselect([x < 3, x > 3],
+    ...             [lambda x, a: x**2, lambda x, a: a * x**3],
+    ...             (x, a), default=np.nan)
+    array([   0.,    1.,    4.,   nan,  -64., -125.])
+
+    """
+    arrays = np.broadcast_arrays(*arrays)
+    tcode = np.mintypecode([a.dtype.char for a in arrays])
+    out = np.full(np.shape(arrays[0]), fill_value=default, dtype=tcode)
+    for func, cond in zip(choicelist, condlist):
+        if np.all(cond is False):
+            continue
+        cond, _ = np.broadcast_arrays(cond, arrays[0])
+        temp = tuple(np.extract(cond, arr) for arr in arrays)
+        np.place(out, cond, func(*temp))
+    return out
+
+
+def _aligned_zeros(shape, dtype=float, order="C", align=None):
+    """Allocate a new ndarray with aligned memory.
+
+    Primary use case for this currently is working around a f2py issue
+    in NumPy 1.9.1, where dtype.alignment is such that np.zeros() does
+    not necessarily create arrays aligned up to it.
+
+    """
+    dtype = np.dtype(dtype)
+    if align is None:
+        align = dtype.alignment
+    if not hasattr(shape, '__len__'):
+        shape = (shape,)
+    size = functools.reduce(operator.mul, shape) * dtype.itemsize
+    buf = np.empty(size + align + 1, np.uint8)
+    offset = buf.__array_interface__['data'][0] % align
+    if offset != 0:
+        offset = align - offset
+    # Note: slices producing 0-size arrays do not necessarily change
+    # data pointer --- so we use and allocate size+1
+    buf = buf[offset:offset+size+1][:-1]
+    data = np.ndarray(shape, dtype, buf, order=order)
+    data.fill(0)
+    return data
+
+
+def _prune_array(array):
+    """Return an array equivalent to the input array. If the input
+    array is a view of a much larger array, copy its contents to a
+    newly allocated array. Otherwise, return the input unchanged.
+    """
+    if array.base is not None and array.size < array.base.size // 2:
+        return array.copy()
+    return array
+
+
+def float_factorial(n: int) -> float:
+    """Compute the factorial and return as a float
+
+    Returns infinity when result is too large for a double
+    """
+    return float(math.factorial(n)) if n < 171 else np.inf
+
+
+# copy-pasted from scikit-learn utils/validation.py
+# change this to scipy.stats._qmc.check_random_state once numpy 1.16 is dropped
+def check_random_state(seed):
+    """Turn `seed` into a `np.random.RandomState` instance.
+
+    Parameters
+    ----------
+    seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional
+        If `seed` is None (or `np.random`), the `numpy.random.RandomState`
+        singleton is used.
+        If `seed` is an int, a new ``RandomState`` instance is used,
+        seeded with `seed`.
+        If `seed` is already a ``Generator`` or ``RandomState`` instance then
+        that instance is used.
+
+    Returns
+    -------
+    seed : {`numpy.random.Generator`, `numpy.random.RandomState`}
+        Random number generator.
+
+    """
+    if seed is None or seed is np.random:
+        return np.random.mtrand._rand
+    if isinstance(seed, (numbers.Integral, np.integer)):
+        return np.random.RandomState(seed)
+    if isinstance(seed, (np.random.RandomState, np.random.Generator)):
+        return seed
+
+    raise ValueError(f"'{seed}' cannot be used to seed a numpy.random.RandomState"
+                     " instance")
+
+
+def _asarray_validated(a, check_finite=True,
+                       sparse_ok=False, objects_ok=False, mask_ok=False,
+                       as_inexact=False):
+    """
+    Helper function for SciPy argument validation.
+
+    Many SciPy linear algebra functions do support arbitrary array-like
+    input arguments. Examples of commonly unsupported inputs include
+    matrices containing inf/nan, sparse matrix representations, and
+    matrices with complicated elements.
+
+    Parameters
+    ----------
+    a : array_like
+        The array-like input.
+    check_finite : bool, optional
+        Whether to check that the input matrices contain only finite numbers.
+        Disabling may give a performance gain, but may result in problems
+        (crashes, non-termination) if the inputs do contain infinities or NaNs.
+        Default: True
+    sparse_ok : bool, optional
+        True if scipy sparse matrices are allowed.
+    objects_ok : bool, optional
+        True if arrays with dype('O') are allowed.
+    mask_ok : bool, optional
+        True if masked arrays are allowed.
+    as_inexact : bool, optional
+        True to convert the input array to a np.inexact dtype.
+
+    Returns
+    -------
+    ret : ndarray
+        The converted validated array.
+
+    """
+    if not sparse_ok:
+        import scipy.sparse
+        if scipy.sparse.issparse(a):
+            msg = ('Sparse matrices are not supported by this function. '
+                   'Perhaps one of the scipy.sparse.linalg functions '
+                   'would work instead.')
+            raise ValueError(msg)
+    if not mask_ok:
+        if np.ma.isMaskedArray(a):
+            raise ValueError('masked arrays are not supported')
+    toarray = np.asarray_chkfinite if check_finite else np.asarray
+    a = toarray(a)
+    if not objects_ok:
+        if a.dtype is np.dtype('O'):
+            raise ValueError('object arrays are not supported')
+    if as_inexact:
+        if not np.issubdtype(a.dtype, np.inexact):
+            a = toarray(a, dtype=np.float64)
+    return a
+
+
+def _validate_int(k, name, minimum=None):
+    """
+    Validate a scalar integer.
+
+    This function can be used to validate an argument to a function
+    that expects the value to be an integer.  It uses `operator.index`
+    to validate the value (so, for example, k=2.0 results in a
+    TypeError).
+
+    Parameters
+    ----------
+    k : int
+        The value to be validated.
+    name : str
+        The name of the parameter.
+    minimum : int, optional
+        An optional lower bound.
+    """
+    try:
+        k = operator.index(k)
+    except TypeError:
+        raise TypeError(f'{name} must be an integer.') from None
+    if minimum is not None and k < minimum:
+        raise ValueError(f'{name} must be an integer not less '
+                         f'than {minimum}') from None
+    return k
+
+
+# Add a replacement for inspect.getfullargspec()/
+# The version below is borrowed from Django,
+# https://github.com/django/django/pull/4846.
+
+# Note an inconsistency between inspect.getfullargspec(func) and
+# inspect.signature(func). If `func` is a bound method, the latter does *not*
+# list `self` as a first argument, while the former *does*.
+# Hence, cook up a common ground replacement: `getfullargspec_no_self` which
+# mimics `inspect.getfullargspec` but does not list `self`.
+#
+# This way, the caller code does not need to know whether it uses a legacy
+# .getfullargspec or a bright and shiny .signature.
+
+FullArgSpec = namedtuple('FullArgSpec',
+                         ['args', 'varargs', 'varkw', 'defaults',
+                          'kwonlyargs', 'kwonlydefaults', 'annotations'])
+
+
+def getfullargspec_no_self(func):
+    """inspect.getfullargspec replacement using inspect.signature.
+
+    If func is a bound method, do not list the 'self' parameter.
+
+    Parameters
+    ----------
+    func : callable
+        A callable to inspect
+
+    Returns
+    -------
+    fullargspec : FullArgSpec(args, varargs, varkw, defaults, kwonlyargs,
+                              kwonlydefaults, annotations)
+
+        NOTE: if the first argument of `func` is self, it is *not*, I repeat
+        *not*, included in fullargspec.args.
+        This is done for consistency between inspect.getargspec() under
+        Python 2.x, and inspect.signature() under Python 3.x.
+
+    """
+    sig = inspect.signature(func)
+    args = [
+        p.name for p in sig.parameters.values()
+        if p.kind in [inspect.Parameter.POSITIONAL_OR_KEYWORD,
+                      inspect.Parameter.POSITIONAL_ONLY]
+    ]
+    varargs = [
+        p.name for p in sig.parameters.values()
+        if p.kind == inspect.Parameter.VAR_POSITIONAL
+    ]
+    varargs = varargs[0] if varargs else None
+    varkw = [
+        p.name for p in sig.parameters.values()
+        if p.kind == inspect.Parameter.VAR_KEYWORD
+    ]
+    varkw = varkw[0] if varkw else None
+    defaults = tuple(
+        p.default for p in sig.parameters.values()
+        if (p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD and
+            p.default is not p.empty)
+    ) or None
+    kwonlyargs = [
+        p.name for p in sig.parameters.values()
+        if p.kind == inspect.Parameter.KEYWORD_ONLY
+    ]
+    kwdefaults = {p.name: p.default for p in sig.parameters.values()
+                  if p.kind == inspect.Parameter.KEYWORD_ONLY and
+                  p.default is not p.empty}
+    annotations = {p.name: p.annotation for p in sig.parameters.values()
+                   if p.annotation is not p.empty}
+    return FullArgSpec(args, varargs, varkw, defaults, kwonlyargs,
+                       kwdefaults or None, annotations)
+
+
+class _FunctionWrapper:
+    """
+    Object to wrap user's function, allowing picklability
+    """
+    def __init__(self, f, args):
+        self.f = f
+        self.args = [] if args is None else args
+
+    def __call__(self, x):
+        return self.f(x, *self.args)
+
+
+class MapWrapper:
+    """
+    Parallelisation wrapper for working with map-like callables, such as
+    `multiprocessing.Pool.map`.
+
+    Parameters
+    ----------
+    pool : int or map-like callable
+        If `pool` is an integer, then it specifies the number of threads to
+        use for parallelization. If ``int(pool) == 1``, then no parallel
+        processing is used and the map builtin is used.
+        If ``pool == -1``, then the pool will utilize all available CPUs.
+        If `pool` is a map-like callable that follows the same
+        calling sequence as the built-in map function, then this callable is
+        used for parallelization.
+    """
+    def __init__(self, pool=1):
+        self.pool = None
+        self._mapfunc = map
+        self._own_pool = False
+
+        if callable(pool):
+            self.pool = pool
+            self._mapfunc = self.pool
+        else:
+            from multiprocessing import Pool
+            # user supplies a number
+            if int(pool) == -1:
+                # use as many processors as possible
+                self.pool = Pool()
+                self._mapfunc = self.pool.map
+                self._own_pool = True
+            elif int(pool) == 1:
+                pass
+            elif int(pool) > 1:
+                # use the number of processors requested
+                self.pool = Pool(processes=int(pool))
+                self._mapfunc = self.pool.map
+                self._own_pool = True
+            else:
+                raise RuntimeError("Number of workers specified must be -1,"
+                                   " an int >= 1, or an object with a 'map' "
+                                   "method")
+
+    def __enter__(self):
+        return self
+
+    def terminate(self):
+        if self._own_pool:
+            self.pool.terminate()
+
+    def join(self):
+        if self._own_pool:
+            self.pool.join()
+
+    def close(self):
+        if self._own_pool:
+            self.pool.close()
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        if self._own_pool:
+            self.pool.close()
+            self.pool.terminate()
+
+    def __call__(self, func, iterable):
+        # only accept one iterable because that's all Pool.map accepts
+        try:
+            return self._mapfunc(func, iterable)
+        except TypeError as e:
+            # wrong number of arguments
+            raise TypeError("The map-like callable must be of the"
+                            " form f(func, iterable)") from e
+
+
+def rng_integers(gen, low, high=None, size=None, dtype='int64',
+                 endpoint=False):
+    """
+    Return random integers from low (inclusive) to high (exclusive), or if
+    endpoint=True, low (inclusive) to high (inclusive). Replaces
+    `RandomState.randint` (with endpoint=False) and
+    `RandomState.random_integers` (with endpoint=True).
+
+    Return random integers from the "discrete uniform" distribution of the
+    specified dtype. If high is None (the default), then results are from
+    0 to low.
+
+    Parameters
+    ----------
+    gen : {None, np.random.RandomState, np.random.Generator}
+        Random number generator. If None, then the np.random.RandomState
+        singleton is used.
+    low : int or array-like of ints
+        Lowest (signed) integers to be drawn from the distribution (unless
+        high=None, in which case this parameter is 0 and this value is used
+        for high).
+    high : int or array-like of ints
+        If provided, one above the largest (signed) integer to be drawn from
+        the distribution (see above for behavior if high=None). If array-like,
+        must contain integer values.
+    size : array-like of ints, optional
+        Output shape. If the given shape is, e.g., (m, n, k), then m * n * k
+        samples are drawn. Default is None, in which case a single value is
+        returned.
+    dtype : {str, dtype}, optional
+        Desired dtype of the result. All dtypes are determined by their name,
+        i.e., 'int64', 'int', etc, so byteorder is not available and a specific
+        precision may have different C types depending on the platform.
+        The default value is 'int64'.
+    endpoint : bool, optional
+        If True, sample from the interval [low, high] instead of the default
+        [low, high) Defaults to False.
+
+    Returns
+    -------
+    out: int or ndarray of ints
+        size-shaped array of random integers from the appropriate distribution,
+        or a single such random int if size not provided.
+    """
+    if isinstance(gen, Generator):
+        return gen.integers(low, high=high, size=size, dtype=dtype,
+                            endpoint=endpoint)
+    else:
+        if gen is None:
+            # default is RandomState singleton used by np.random.
+            gen = np.random.mtrand._rand
+        if endpoint:
+            # inclusive of endpoint
+            # remember that low and high can be arrays, so don't modify in
+            # place
+            if high is None:
+                return gen.randint(low + 1, size=size, dtype=dtype)
+            if high is not None:
+                return gen.randint(low, high=high + 1, size=size, dtype=dtype)
+
+        # exclusive
+        return gen.randint(low, high=high, size=size, dtype=dtype)
+
+
+@contextmanager
+def _fixed_default_rng(seed=1638083107694713882823079058616272161):
+    """Context with a fixed np.random.default_rng seed."""
+    orig_fun = np.random.default_rng
+    np.random.default_rng = lambda seed=seed: orig_fun(seed)
+    try:
+        yield
+    finally:
+        np.random.default_rng = orig_fun
+
+
+def _rng_html_rewrite(func):
+    """Rewrite the HTML rendering of ``np.random.default_rng``.
+
+    This is intended to decorate
+    ``numpydoc.docscrape_sphinx.SphinxDocString._str_examples``.
+
+    Examples are only run by Sphinx when there are plot involved. Even so,
+    it does not change the result values getting printed.
+    """
+    # hexadecimal or number seed, case-insensitive
+    pattern = re.compile(r'np.random.default_rng\((0x[0-9A-F]+|\d+)\)', re.I)
+
+    def _wrapped(*args, **kwargs):
+        res = func(*args, **kwargs)
+        lines = [
+            re.sub(pattern, 'np.random.default_rng()', line)
+            for line in res
+        ]
+        return lines
+
+    return _wrapped
+
+
+def _argmin(a, keepdims=False, axis=None):
+    """
+    argmin with a `keepdims` parameter.
+
+    See https://github.com/numpy/numpy/issues/8710
+
+    If axis is not None, a.shape[axis] must be greater than 0.
+    """
+    res = np.argmin(a, axis=axis)
+    if keepdims and axis is not None:
+        res = np.expand_dims(res, axis=axis)
+    return res
+
+
+def _first_nonnan(a, axis):
+    """
+    Return the first non-nan value along the given axis.
+
+    If a slice is all nan, nan is returned for that slice.
+
+    The shape of the return value corresponds to ``keepdims=True``.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> nan = np.nan
+    >>> a = np.array([[ 3.,  3., nan,  3.],
+                      [ 1., nan,  2.,  4.],
+                      [nan, nan,  9., -1.],
+                      [nan,  5.,  4.,  3.],
+                      [ 2.,  2.,  2.,  2.],
+                      [nan, nan, nan, nan]])
+    >>> _first_nonnan(a, axis=0)
+    array([[3., 3., 2., 3.]])
+    >>> _first_nonnan(a, axis=1)
+    array([[ 3.],
+           [ 1.],
+           [ 9.],
+           [ 5.],
+           [ 2.],
+           [nan]])
+    """
+    k = _argmin(np.isnan(a), axis=axis, keepdims=True)
+    return np.take_along_axis(a, k, axis=axis)
+
+
+def _nan_allsame(a, axis, keepdims=False):
+    """
+    Determine if the values along an axis are all the same.
+
+    nan values are ignored.
+
+    `a` must be a numpy array.
+
+    `axis` is assumed to be normalized; that is, 0 <= axis < a.ndim.
+
+    For an axis of length 0, the result is True.  That is, we adopt the
+    convention that ``allsame([])`` is True. (There are no values in the
+    input that are different.)
+
+    `True` is returned for slices that are all nan--not because all the
+    values are the same, but because this is equivalent to ``allsame([])``.
+
+    Examples
+    --------
+    >>> from numpy import nan, array
+    >>> a = array([[ 3.,  3., nan,  3.],
+    ...            [ 1., nan,  2.,  4.],
+    ...            [nan, nan,  9., -1.],
+    ...            [nan,  5.,  4.,  3.],
+    ...            [ 2.,  2.,  2.,  2.],
+    ...            [nan, nan, nan, nan]])
+    >>> _nan_allsame(a, axis=1, keepdims=True)
+    array([[ True],
+           [False],
+           [False],
+           [False],
+           [ True],
+           [ True]])
+    """
+    if axis is None:
+        if a.size == 0:
+            return True
+        a = a.ravel()
+        axis = 0
+    else:
+        shp = a.shape
+        if shp[axis] == 0:
+            shp = shp[:axis] + (1,)*keepdims + shp[axis + 1:]
+            return np.full(shp, fill_value=True, dtype=bool)
+    a0 = _first_nonnan(a, axis=axis)
+    return ((a0 == a) | np.isnan(a)).all(axis=axis, keepdims=keepdims)
+
+
+def _contains_nan(a, nan_policy='propagate', policies=None, *, xp=None):
+    if xp is None:
+        xp = array_namespace(a)
+    not_numpy = not is_numpy(xp)
+
+    if policies is None:
+        policies = {'propagate', 'raise', 'omit'}
+    if nan_policy not in policies:
+        raise ValueError(f"nan_policy must be one of {set(policies)}.")
+
+    inexact = (xp.isdtype(a.dtype, "real floating")
+               or xp.isdtype(a.dtype, "complex floating"))
+    if xp_size(a) == 0:
+        contains_nan = False
+    elif inexact:
+        # Faster and less memory-intensive than xp.any(xp.isnan(a))
+        contains_nan = xp.isnan(xp.max(a))
+    elif is_numpy(xp) and np.issubdtype(a.dtype, object):
+        contains_nan = False
+        for el in a.ravel():
+            # isnan doesn't work on non-numeric elements
+            if np.issubdtype(type(el), np.number) and np.isnan(el):
+                contains_nan = True
+                break
+    else:
+        # Only `object` and `inexact` arrays can have NaNs
+        contains_nan = False
+
+    if contains_nan and nan_policy == 'raise':
+        raise ValueError("The input contains nan values")
+
+    if not_numpy and contains_nan and nan_policy=='omit':
+        message = "`nan_policy='omit' is incompatible with non-NumPy arrays."
+        raise ValueError(message)
+
+    return contains_nan, nan_policy
+
+
+def _rename_parameter(old_name, new_name, dep_version=None):
+    """
+    Generate decorator for backward-compatible keyword renaming.
+
+    Apply the decorator generated by `_rename_parameter` to functions with a
+    recently renamed parameter to maintain backward-compatibility.
+
+    After decoration, the function behaves as follows:
+    If only the new parameter is passed into the function, behave as usual.
+    If only the old parameter is passed into the function (as a keyword), raise
+    a DeprecationWarning if `dep_version` is provided, and behave as usual
+    otherwise.
+    If both old and new parameters are passed into the function, raise a
+    DeprecationWarning if `dep_version` is provided, and raise the appropriate
+    TypeError (function got multiple values for argument).
+
+    Parameters
+    ----------
+    old_name : str
+        Old name of parameter
+    new_name : str
+        New name of parameter
+    dep_version : str, optional
+        Version of SciPy in which old parameter was deprecated in the format
+        'X.Y.Z'. If supplied, the deprecation message will indicate that
+        support for the old parameter will be removed in version 'X.Y+2.Z'
+
+    Notes
+    -----
+    Untested with functions that accept *args. Probably won't work as written.
+
+    """
+    def decorator(fun):
+        @functools.wraps(fun)
+        def wrapper(*args, **kwargs):
+            if old_name in kwargs:
+                if dep_version:
+                    end_version = dep_version.split('.')
+                    end_version[1] = str(int(end_version[1]) + 2)
+                    end_version = '.'.join(end_version)
+                    message = (f"Use of keyword argument `{old_name}` is "
+                               f"deprecated and replaced by `{new_name}`.  "
+                               f"Support for `{old_name}` will be removed "
+                               f"in SciPy {end_version}.")
+                    warnings.warn(message, DeprecationWarning, stacklevel=2)
+                if new_name in kwargs:
+                    message = (f"{fun.__name__}() got multiple values for "
+                               f"argument now known as `{new_name}`")
+                    raise TypeError(message)
+                kwargs[new_name] = kwargs.pop(old_name)
+            return fun(*args, **kwargs)
+        return wrapper
+    return decorator
+
+
+def _rng_spawn(rng, n_children):
+    # spawns independent RNGs from a parent RNG
+    bg = rng._bit_generator
+    ss = bg._seed_seq
+    child_rngs = [np.random.Generator(type(bg)(child_ss))
+                  for child_ss in ss.spawn(n_children)]
+    return child_rngs
+
+
+def _get_nan(*data, xp=None):
+    xp = array_namespace(*data) if xp is None else xp
+    # Get NaN of appropriate dtype for data
+    data = [xp.asarray(item) for item in data]
+    try:
+        min_float = getattr(xp, 'float16', xp.float32)
+        dtype = xp.result_type(*data, min_float)  # must be at least a float
+    except DTypePromotionError:
+        # fallback to float64
+        dtype = xp.float64
+    return xp.asarray(xp.nan, dtype=dtype)[()]
+
+
+def normalize_axis_index(axis, ndim):
+    # Check if `axis` is in the correct range and normalize it
+    if axis < -ndim or axis >= ndim:
+        msg = f"axis {axis} is out of bounds for array of dimension {ndim}"
+        raise AxisError(msg)
+
+    if axis < 0:
+        axis = axis + ndim
+    return axis
+
+
+def _call_callback_maybe_halt(callback, res):
+    """Call wrapped callback; return True if algorithm should stop.
+
+    Parameters
+    ----------
+    callback : callable or None
+        A user-provided callback wrapped with `_wrap_callback`
+    res : OptimizeResult
+        Information about the current iterate
+
+    Returns
+    -------
+    halt : bool
+        True if minimization should stop
+
+    """
+    if callback is None:
+        return False
+    try:
+        callback(res)
+        return False
+    except StopIteration:
+        callback.stop_iteration = True
+        return True
+
+
+class _RichResult(dict):
+    """ Container for multiple outputs with pretty-printing """
+    def __getattr__(self, name):
+        try:
+            return self[name]
+        except KeyError as e:
+            raise AttributeError(name) from e
+
+    __setattr__ = dict.__setitem__  # type: ignore[assignment]
+    __delattr__ = dict.__delitem__  # type: ignore[assignment]
+
+    def __repr__(self):
+        order_keys = ['message', 'success', 'status', 'fun', 'funl', 'x', 'xl',
+                      'col_ind', 'nit', 'lower', 'upper', 'eqlin', 'ineqlin',
+                      'converged', 'flag', 'function_calls', 'iterations',
+                      'root']
+        order_keys = getattr(self, '_order_keys', order_keys)
+        # 'slack', 'con' are redundant with residuals
+        # 'crossover_nit' is probably not interesting to most users
+        omit_keys = {'slack', 'con', 'crossover_nit', '_order_keys'}
+
+        def key(item):
+            try:
+                return order_keys.index(item[0].lower())
+            except ValueError:  # item not in list
+                return np.inf
+
+        def omit_redundant(items):
+            for item in items:
+                if item[0] in omit_keys:
+                    continue
+                yield item
+
+        def item_sorter(d):
+            return sorted(omit_redundant(d.items()), key=key)
+
+        if self.keys():
+            return _dict_formatter(self, sorter=item_sorter)
+        else:
+            return self.__class__.__name__ + "()"
+
+    def __dir__(self):
+        return list(self.keys())
+
+
+def _indenter(s, n=0):
+    """
+    Ensures that lines after the first are indented by the specified amount
+    """
+    split = s.split("\n")
+    indent = " "*n
+    return ("\n" + indent).join(split)
+
+
+def _float_formatter_10(x):
+    """
+    Returns a string representation of a float with exactly ten characters
+    """
+    if np.isposinf(x):
+        return "       inf"
+    elif np.isneginf(x):
+        return "      -inf"
+    elif np.isnan(x):
+        return "       nan"
+    return np.format_float_scientific(x, precision=3, pad_left=2, unique=False)
+
+
+def _dict_formatter(d, n=0, mplus=1, sorter=None):
+    """
+    Pretty printer for dictionaries
+
+    `n` keeps track of the starting indentation;
+    lines are indented by this much after a line break.
+    `mplus` is additional left padding applied to keys
+    """
+    if isinstance(d, dict):
+        m = max(map(len, list(d.keys()))) + mplus  # width to print keys
+        s = '\n'.join([k.rjust(m) + ': ' +  # right justified, width m
+                       _indenter(_dict_formatter(v, m+n+2, 0, sorter), m+2)
+                       for k, v in sorter(d)])  # +2 for ': '
+    else:
+        # By default, NumPy arrays print with linewidth=76. `n` is
+        # the indent at which a line begins printing, so it is subtracted
+        # from the default to avoid exceeding 76 characters total.
+        # `edgeitems` is the number of elements to include before and after
+        # ellipses when arrays are not shown in full.
+        # `threshold` is the maximum number of elements for which an
+        # array is shown in full.
+        # These values tend to work well for use with OptimizeResult.
+        with np.printoptions(linewidth=76-n, edgeitems=2, threshold=12,
+                             formatter={'float_kind': _float_formatter_10}):
+            s = str(d)
+    return s
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/decorator.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/decorator.py
new file mode 100644
index 0000000000000000000000000000000000000000..02121774d3c2a9407a73366bb3e5915387a571d0
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/decorator.py
@@ -0,0 +1,399 @@
+# #########################     LICENSE     ############################ #
+
+# Copyright (c) 2005-2015, Michele Simionato
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+
+#   Redistributions of source code must retain the above copyright
+#   notice, this list of conditions and the following disclaimer.
+#   Redistributions in bytecode form must reproduce the above copyright
+#   notice, this list of conditions and the following disclaimer in
+#   the documentation and/or other materials provided with the
+#   distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+"""
+Decorator module, see https://pypi.python.org/pypi/decorator
+for the documentation.
+"""
+import re
+import sys
+import inspect
+import operator
+import itertools
+import collections
+
+from inspect import getfullargspec
+
+__version__ = '4.0.5'
+
+
+def get_init(cls):
+    return cls.__init__
+
+
+# getargspec has been deprecated in Python 3.5
+ArgSpec = collections.namedtuple(
+    'ArgSpec', 'args varargs varkw defaults')
+
+
+def getargspec(f):
+    """A replacement for inspect.getargspec"""
+    spec = getfullargspec(f)
+    return ArgSpec(spec.args, spec.varargs, spec.varkw, spec.defaults)
+
+
+DEF = re.compile(r'\s*def\s*([_\w][_\w\d]*)\s*\(')
+
+
+# basic functionality
+class FunctionMaker:
+    """
+    An object with the ability to create functions with a given signature.
+    It has attributes name, doc, module, signature, defaults, dict, and
+    methods update and make.
+    """
+
+    # Atomic get-and-increment provided by the GIL
+    _compile_count = itertools.count()
+
+    def __init__(self, func=None, name=None, signature=None,
+                 defaults=None, doc=None, module=None, funcdict=None):
+        self.shortsignature = signature
+        if func:
+            # func can be a class or a callable, but not an instance method
+            self.name = func.__name__
+            if self.name == '':  # small hack for lambda functions
+                self.name = '_lambda_'
+            self.doc = func.__doc__
+            self.module = func.__module__
+            if inspect.isfunction(func):
+                argspec = getfullargspec(func)
+                self.annotations = getattr(func, '__annotations__', {})
+                for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs',
+                          'kwonlydefaults'):
+                    setattr(self, a, getattr(argspec, a))
+                for i, arg in enumerate(self.args):
+                    setattr(self, 'arg%d' % i, arg)
+                allargs = list(self.args)
+                allshortargs = list(self.args)
+                if self.varargs:
+                    allargs.append('*' + self.varargs)
+                    allshortargs.append('*' + self.varargs)
+                elif self.kwonlyargs:
+                    allargs.append('*')  # single star syntax
+                for a in self.kwonlyargs:
+                    allargs.append('%s=None' % a)
+                    allshortargs.append(f'{a}={a}')
+                if self.varkw:
+                    allargs.append('**' + self.varkw)
+                    allshortargs.append('**' + self.varkw)
+                self.signature = ', '.join(allargs)
+                self.shortsignature = ', '.join(allshortargs)
+                self.dict = func.__dict__.copy()
+        # func=None happens when decorating a caller
+        if name:
+            self.name = name
+        if signature is not None:
+            self.signature = signature
+        if defaults:
+            self.defaults = defaults
+        if doc:
+            self.doc = doc
+        if module:
+            self.module = module
+        if funcdict:
+            self.dict = funcdict
+        # check existence required attributes
+        assert hasattr(self, 'name')
+        if not hasattr(self, 'signature'):
+            raise TypeError('You are decorating a non-function: %s' % func)
+
+    def update(self, func, **kw):
+        "Update the signature of func with the data in self"
+        func.__name__ = self.name
+        func.__doc__ = getattr(self, 'doc', None)
+        func.__dict__ = getattr(self, 'dict', {})
+        func.__defaults__ = getattr(self, 'defaults', ())
+        func.__kwdefaults__ = getattr(self, 'kwonlydefaults', None)
+        func.__annotations__ = getattr(self, 'annotations', None)
+        try:
+            frame = sys._getframe(3)
+        except AttributeError:  # for IronPython and similar implementations
+            callermodule = '?'
+        else:
+            callermodule = frame.f_globals.get('__name__', '?')
+        func.__module__ = getattr(self, 'module', callermodule)
+        func.__dict__.update(kw)
+
+    def make(self, src_templ, evaldict=None, addsource=False, **attrs):
+        "Make a new function from a given template and update the signature"
+        src = src_templ % vars(self)  # expand name and signature
+        evaldict = evaldict or {}
+        mo = DEF.match(src)
+        if mo is None:
+            raise SyntaxError('not a valid function template\n%s' % src)
+        name = mo.group(1)  # extract the function name
+        names = set([name] + [arg.strip(' *') for arg in
+                              self.shortsignature.split(',')])
+        for n in names:
+            if n in ('_func_', '_call_'):
+                raise NameError(f'{n} is overridden in\n{src}')
+        if not src.endswith('\n'):  # add a newline just for safety
+            src += '\n'  # this is needed in old versions of Python
+
+        # Ensure each generated function has a unique filename for profilers
+        # (such as cProfile) that depend on the tuple of (,
+        # , ) being unique.
+        filename = '' % (next(self._compile_count),)
+        try:
+            code = compile(src, filename, 'single')
+            exec(code, evaldict)
+        except:  # noqa: E722
+            print('Error in generated code:', file=sys.stderr)
+            print(src, file=sys.stderr)
+            raise
+        func = evaldict[name]
+        if addsource:
+            attrs['__source__'] = src
+        self.update(func, **attrs)
+        return func
+
+    @classmethod
+    def create(cls, obj, body, evaldict, defaults=None,
+               doc=None, module=None, addsource=True, **attrs):
+        """
+        Create a function from the strings name, signature, and body.
+        evaldict is the evaluation dictionary. If addsource is true, an
+        attribute __source__ is added to the result. The attributes attrs
+        are added, if any.
+        """
+        if isinstance(obj, str):  # "name(signature)"
+            name, rest = obj.strip().split('(', 1)
+            signature = rest[:-1]  # strip a right parens
+            func = None
+        else:  # a function
+            name = None
+            signature = None
+            func = obj
+        self = cls(func, name, signature, defaults, doc, module)
+        ibody = '\n'.join('    ' + line for line in body.splitlines())
+        return self.make('def %(name)s(%(signature)s):\n' + ibody,
+                         evaldict, addsource, **attrs)
+
+
+def decorate(func, caller):
+    """
+    decorate(func, caller) decorates a function using a caller.
+    """
+    evaldict = func.__globals__.copy()
+    evaldict['_call_'] = caller
+    evaldict['_func_'] = func
+    fun = FunctionMaker.create(
+        func, "return _call_(_func_, %(shortsignature)s)",
+        evaldict, __wrapped__=func)
+    if hasattr(func, '__qualname__'):
+        fun.__qualname__ = func.__qualname__
+    return fun
+
+
+def decorator(caller, _func=None):
+    """decorator(caller) converts a caller function into a decorator"""
+    if _func is not None:  # return a decorated function
+        # this is obsolete behavior; you should use decorate instead
+        return decorate(_func, caller)
+    # else return a decorator function
+    if inspect.isclass(caller):
+        name = caller.__name__.lower()
+        callerfunc = get_init(caller)
+        doc = (f'decorator({caller.__name__}) converts functions/generators into ' 
+               f'factories of {caller.__name__} objects')
+    elif inspect.isfunction(caller):
+        if caller.__name__ == '':
+            name = '_lambda_'
+        else:
+            name = caller.__name__
+        callerfunc = caller
+        doc = caller.__doc__
+    else:  # assume caller is an object with a __call__ method
+        name = caller.__class__.__name__.lower()
+        callerfunc = caller.__call__.__func__
+        doc = caller.__call__.__doc__
+    evaldict = callerfunc.__globals__.copy()
+    evaldict['_call_'] = caller
+    evaldict['_decorate_'] = decorate
+    return FunctionMaker.create(
+        '%s(func)' % name, 'return _decorate_(func, _call_)',
+        evaldict, doc=doc, module=caller.__module__,
+        __wrapped__=caller)
+
+
+# ####################### contextmanager ####################### #
+
+try:  # Python >= 3.2
+    from contextlib import _GeneratorContextManager
+except ImportError:  # Python >= 2.5
+    from contextlib import GeneratorContextManager as _GeneratorContextManager
+
+
+class ContextManager(_GeneratorContextManager):
+    def __call__(self, func):
+        """Context manager decorator"""
+        return FunctionMaker.create(
+            func, "with _self_: return _func_(%(shortsignature)s)",
+            dict(_self_=self, _func_=func), __wrapped__=func)
+
+
+init = getfullargspec(_GeneratorContextManager.__init__)
+n_args = len(init.args)
+if n_args == 2 and not init.varargs:  # (self, genobj) Python 2.7
+    def __init__(self, g, *a, **k):
+        return _GeneratorContextManager.__init__(self, g(*a, **k))
+    ContextManager.__init__ = __init__
+elif n_args == 2 and init.varargs:  # (self, gen, *a, **k) Python 3.4
+    pass
+elif n_args == 4:  # (self, gen, args, kwds) Python 3.5
+    def __init__(self, g, *a, **k):
+        return _GeneratorContextManager.__init__(self, g, a, k)
+    ContextManager.__init__ = __init__
+
+contextmanager = decorator(ContextManager)
+
+
+# ############################ dispatch_on ############################ #
+
+def append(a, vancestors):
+    """
+    Append ``a`` to the list of the virtual ancestors, unless it is already
+    included.
+    """
+    add = True
+    for j, va in enumerate(vancestors):
+        if issubclass(va, a):
+            add = False
+            break
+        if issubclass(a, va):
+            vancestors[j] = a
+            add = False
+    if add:
+        vancestors.append(a)
+
+
+# inspired from simplegeneric by P.J. Eby and functools.singledispatch
+def dispatch_on(*dispatch_args):
+    """
+    Factory of decorators turning a function into a generic function
+    dispatching on the given arguments.
+    """
+    assert dispatch_args, 'No dispatch args passed'
+    dispatch_str = '(%s,)' % ', '.join(dispatch_args)
+
+    def check(arguments, wrong=operator.ne, msg=''):
+        """Make sure one passes the expected number of arguments"""
+        if wrong(len(arguments), len(dispatch_args)):
+            raise TypeError('Expected %d arguments, got %d%s' %
+                            (len(dispatch_args), len(arguments), msg))
+
+    def gen_func_dec(func):
+        """Decorator turning a function into a generic function"""
+
+        # first check the dispatch arguments
+        argset = set(getfullargspec(func).args)
+        if not set(dispatch_args) <= argset:
+            raise NameError('Unknown dispatch arguments %s' % dispatch_str)
+
+        typemap = {}
+
+        def vancestors(*types):
+            """
+            Get a list of sets of virtual ancestors for the given types
+            """
+            check(types)
+            ras = [[] for _ in range(len(dispatch_args))]
+            for types_ in typemap:
+                for t, type_, ra in zip(types, types_, ras):
+                    if issubclass(t, type_) and type_ not in t.__mro__:
+                        append(type_, ra)
+            return [set(ra) for ra in ras]
+
+        def ancestors(*types):
+            """
+            Get a list of virtual MROs, one for each type
+            """
+            check(types)
+            lists = []
+            for t, vas in zip(types, vancestors(*types)):
+                n_vas = len(vas)
+                if n_vas > 1:
+                    raise RuntimeError(
+                        f'Ambiguous dispatch for {t}: {vas}')
+                elif n_vas == 1:
+                    va, = vas
+                    mro = type('t', (t, va), {}).__mro__[1:]
+                else:
+                    mro = t.__mro__
+                lists.append(mro[:-1])  # discard t and object
+            return lists
+
+        def register(*types):
+            """
+            Decorator to register an implementation for the given types
+            """
+            check(types)
+
+            def dec(f):
+                check(getfullargspec(f).args, operator.lt, ' in ' + f.__name__)
+                typemap[types] = f
+                return f
+            return dec
+
+        def dispatch_info(*types):
+            """
+            An utility to introspect the dispatch algorithm
+            """
+            check(types)
+            lst = [tuple(a.__name__ for a in anc)
+                   for anc in itertools.product(*ancestors(*types))]
+            return lst
+
+        def _dispatch(dispatch_args, *args, **kw):
+            types = tuple(type(arg) for arg in dispatch_args)
+            try:  # fast path
+                f = typemap[types]
+            except KeyError:
+                pass
+            else:
+                return f(*args, **kw)
+            combinations = itertools.product(*ancestors(*types))
+            next(combinations)  # the first one has been already tried
+            for types_ in combinations:
+                f = typemap.get(types_)
+                if f is not None:
+                    return f(*args, **kw)
+
+            # else call the default implementation
+            return func(*args, **kw)
+
+        return FunctionMaker.create(
+            func, 'return _f_(%s, %%(shortsignature)s)' % dispatch_str,
+            dict(_f_=_dispatch), register=register, default=func,
+            typemap=typemap, vancestors=vancestors, ancestors=ancestors,
+            dispatch_info=dispatch_info, __wrapped__=func)
+
+    gen_func_dec.__name__ = 'dispatch_on' + dispatch_str
+    return gen_func_dec
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/deprecation.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/deprecation.py
new file mode 100644
index 0000000000000000000000000000000000000000..01a1dfa73695f00b409a3adab26dbb98b804b384
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/deprecation.py
@@ -0,0 +1,239 @@
+from inspect import Parameter, signature
+import functools
+import warnings
+from importlib import import_module
+
+
+__all__ = ["_deprecated"]
+
+
+# Object to use as default value for arguments to be deprecated. This should
+# be used over 'None' as the user could parse 'None' as a positional argument
+_NoValue = object()
+
+def _sub_module_deprecation(*, sub_package, module, private_modules, all,
+                            attribute, correct_module=None):
+    """Helper function for deprecating modules that are public but were
+    intended to be private.
+
+    Parameters
+    ----------
+    sub_package : str
+        Subpackage the module belongs to eg. stats
+    module : str
+        Public but intended private module to deprecate
+    private_modules : list
+        Private replacement(s) for `module`; should contain the
+        content of ``all``, possibly spread over several modules.
+    all : list
+        ``__all__`` belonging to `module`
+    attribute : str
+        The attribute in `module` being accessed
+    correct_module : str, optional
+        Module in `sub_package` that `attribute` should be imported from.
+        Default is that `attribute` should be imported from ``scipy.sub_package``.
+    """
+    if correct_module is not None:
+        correct_import = f"scipy.{sub_package}.{correct_module}"
+    else:
+        correct_import = f"scipy.{sub_package}"
+
+    if attribute not in all:
+        raise AttributeError(
+            f"`scipy.{sub_package}.{module}` has no attribute `{attribute}`; "
+            f"furthermore, `scipy.{sub_package}.{module}` is deprecated "
+            f"and will be removed in SciPy 2.0.0."
+        )
+
+    attr = getattr(import_module(correct_import), attribute, None)
+
+    if attr is not None:
+        message = (
+            f"Please import `{attribute}` from the `{correct_import}` namespace; "
+            f"the `scipy.{sub_package}.{module}` namespace is deprecated "
+            f"and will be removed in SciPy 2.0.0."
+        )
+    else:
+        message = (
+            f"`scipy.{sub_package}.{module}.{attribute}` is deprecated along with "
+            f"the `scipy.{sub_package}.{module}` namespace. "
+            f"`scipy.{sub_package}.{module}.{attribute}` will be removed "
+            f"in SciPy 1.14.0, and the `scipy.{sub_package}.{module}` namespace "
+            f"will be removed in SciPy 2.0.0."
+        )
+
+    warnings.warn(message, category=DeprecationWarning, stacklevel=3)
+
+    for module in private_modules:
+        try:
+            return getattr(import_module(f"scipy.{sub_package}.{module}"), attribute)
+        except AttributeError as e:
+            # still raise an error if the attribute isn't in any of the expected
+            # private modules
+            if module == private_modules[-1]:
+                raise e
+            continue
+    
+
+def _deprecated(msg, stacklevel=2):
+    """Deprecate a function by emitting a warning on use."""
+    def wrap(fun):
+        if isinstance(fun, type):
+            warnings.warn(
+                f"Trying to deprecate class {fun!r}",
+                category=RuntimeWarning, stacklevel=2)
+            return fun
+
+        @functools.wraps(fun)
+        def call(*args, **kwargs):
+            warnings.warn(msg, category=DeprecationWarning,
+                          stacklevel=stacklevel)
+            return fun(*args, **kwargs)
+        call.__doc__ = fun.__doc__
+        return call
+
+    return wrap
+
+
+class _DeprecationHelperStr:
+    """
+    Helper class used by deprecate_cython_api
+    """
+    def __init__(self, content, message):
+        self._content = content
+        self._message = message
+
+    def __hash__(self):
+        return hash(self._content)
+
+    def __eq__(self, other):
+        res = (self._content == other)
+        if res:
+            warnings.warn(self._message, category=DeprecationWarning,
+                          stacklevel=2)
+        return res
+
+
+def deprecate_cython_api(module, routine_name, new_name=None, message=None):
+    """
+    Deprecate an exported cdef function in a public Cython API module.
+
+    Only functions can be deprecated; typedefs etc. cannot.
+
+    Parameters
+    ----------
+    module : module
+        Public Cython API module (e.g. scipy.linalg.cython_blas).
+    routine_name : str
+        Name of the routine to deprecate. May also be a fused-type
+        routine (in which case its all specializations are deprecated).
+    new_name : str
+        New name to include in the deprecation warning message
+    message : str
+        Additional text in the deprecation warning message
+
+    Examples
+    --------
+    Usually, this function would be used in the top-level of the
+    module ``.pyx`` file:
+
+    >>> from scipy._lib.deprecation import deprecate_cython_api
+    >>> import scipy.linalg.cython_blas as mod
+    >>> deprecate_cython_api(mod, "dgemm", "dgemm_new",
+    ...                      message="Deprecated in Scipy 1.5.0")
+    >>> del deprecate_cython_api, mod
+
+    After this, Cython modules that use the deprecated function emit a
+    deprecation warning when they are imported.
+
+    """
+    old_name = f"{module.__name__}.{routine_name}"
+
+    if new_name is None:
+        depdoc = "`%s` is deprecated!" % old_name
+    else:
+        depdoc = f"`{old_name}` is deprecated, use `{new_name}` instead!"
+
+    if message is not None:
+        depdoc += "\n" + message
+
+    d = module.__pyx_capi__
+
+    # Check if the function is a fused-type function with a mangled name
+    j = 0
+    has_fused = False
+    while True:
+        fused_name = f"__pyx_fuse_{j}{routine_name}"
+        if fused_name in d:
+            has_fused = True
+            d[_DeprecationHelperStr(fused_name, depdoc)] = d.pop(fused_name)
+            j += 1
+        else:
+            break
+
+    # If not, apply deprecation to the named routine
+    if not has_fused:
+        d[_DeprecationHelperStr(routine_name, depdoc)] = d.pop(routine_name)
+
+
+# taken from scikit-learn, see
+# https://github.com/scikit-learn/scikit-learn/blob/1.3.0/sklearn/utils/validation.py#L38
+def _deprecate_positional_args(func=None, *, version=None):
+    """Decorator for methods that issues warnings for positional arguments.
+
+    Using the keyword-only argument syntax in pep 3102, arguments after the
+    * will issue a warning when passed as a positional argument.
+
+    Parameters
+    ----------
+    func : callable, default=None
+        Function to check arguments on.
+    version : callable, default=None
+        The version when positional arguments will result in error.
+    """
+    if version is None:
+        msg = "Need to specify a version where signature will be changed"
+        raise ValueError(msg)
+
+    def _inner_deprecate_positional_args(f):
+        sig = signature(f)
+        kwonly_args = []
+        all_args = []
+
+        for name, param in sig.parameters.items():
+            if param.kind == Parameter.POSITIONAL_OR_KEYWORD:
+                all_args.append(name)
+            elif param.kind == Parameter.KEYWORD_ONLY:
+                kwonly_args.append(name)
+
+        @functools.wraps(f)
+        def inner_f(*args, **kwargs):
+            extra_args = len(args) - len(all_args)
+            if extra_args <= 0:
+                return f(*args, **kwargs)
+
+            # extra_args > 0
+            args_msg = [
+                f"{name}={arg}"
+                for name, arg in zip(kwonly_args[:extra_args], args[-extra_args:])
+            ]
+            args_msg = ", ".join(args_msg)
+            warnings.warn(
+                (
+                    f"You are passing {args_msg} as a positional argument. "
+                    "Please change your invocation to use keyword arguments. "
+                    f"From SciPy {version}, passing these as positional "
+                    "arguments will result in an error."
+                ),
+                DeprecationWarning,
+                stacklevel=2,
+            )
+            kwargs.update(zip(sig.parameters, args))
+            return f(**kwargs)
+
+        return inner_f
+
+    if func is not None:
+        return _inner_deprecate_positional_args(func)
+
+    return _inner_deprecate_positional_args
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/doccer.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/doccer.py
new file mode 100644
index 0000000000000000000000000000000000000000..707f97017b81871e3c495a39e47587cf1f17175c
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/doccer.py
@@ -0,0 +1,275 @@
+''' Utilities to allow inserting docstring fragments for common
+parameters into function and method docstrings'''
+
+import sys
+
+__all__ = [
+    'docformat', 'inherit_docstring_from', 'indentcount_lines',
+    'filldoc', 'unindent_dict', 'unindent_string', 'extend_notes_in_docstring',
+    'replace_notes_in_docstring', 'doc_replace'
+]
+
+
+def docformat(docstring, docdict=None):
+    ''' Fill a function docstring from variables in dictionary
+
+    Adapt the indent of the inserted docs
+
+    Parameters
+    ----------
+    docstring : string
+        docstring from function, possibly with dict formatting strings
+    docdict : dict, optional
+        dictionary with keys that match the dict formatting strings
+        and values that are docstring fragments to be inserted. The
+        indentation of the inserted docstrings is set to match the
+        minimum indentation of the ``docstring`` by adding this
+        indentation to all lines of the inserted string, except the
+        first.
+
+    Returns
+    -------
+    outstring : string
+        string with requested ``docdict`` strings inserted
+
+    Examples
+    --------
+    >>> docformat(' Test string with %(value)s', {'value':'inserted value'})
+    ' Test string with inserted value'
+    >>> docstring = 'First line\\n    Second line\\n    %(value)s'
+    >>> inserted_string = "indented\\nstring"
+    >>> docdict = {'value': inserted_string}
+    >>> docformat(docstring, docdict)
+    'First line\\n    Second line\\n    indented\\n    string'
+    '''
+    if not docstring:
+        return docstring
+    if docdict is None:
+        docdict = {}
+    if not docdict:
+        return docstring
+    lines = docstring.expandtabs().splitlines()
+    # Find the minimum indent of the main docstring, after first line
+    if len(lines) < 2:
+        icount = 0
+    else:
+        icount = indentcount_lines(lines[1:])
+    indent = ' ' * icount
+    # Insert this indent to dictionary docstrings
+    indented = {}
+    for name, dstr in docdict.items():
+        lines = dstr.expandtabs().splitlines()
+        try:
+            newlines = [lines[0]]
+            for line in lines[1:]:
+                newlines.append(indent+line)
+            indented[name] = '\n'.join(newlines)
+        except IndexError:
+            indented[name] = dstr
+    return docstring % indented
+
+
+def inherit_docstring_from(cls):
+    """
+    This decorator modifies the decorated function's docstring by
+    replacing occurrences of '%(super)s' with the docstring of the
+    method of the same name from the class `cls`.
+
+    If the decorated method has no docstring, it is simply given the
+    docstring of `cls`s method.
+
+    Parameters
+    ----------
+    cls : Python class or instance
+        A class with a method with the same name as the decorated method.
+        The docstring of the method in this class replaces '%(super)s' in the
+        docstring of the decorated method.
+
+    Returns
+    -------
+    f : function
+        The decorator function that modifies the __doc__ attribute
+        of its argument.
+
+    Examples
+    --------
+    In the following, the docstring for Bar.func created using the
+    docstring of `Foo.func`.
+
+    >>> class Foo:
+    ...     def func(self):
+    ...         '''Do something useful.'''
+    ...         return
+    ...
+    >>> class Bar(Foo):
+    ...     @inherit_docstring_from(Foo)
+    ...     def func(self):
+    ...         '''%(super)s
+    ...         Do it fast.
+    ...         '''
+    ...         return
+    ...
+    >>> b = Bar()
+    >>> b.func.__doc__
+    'Do something useful.\n        Do it fast.\n        '
+
+    """
+    def _doc(func):
+        cls_docstring = getattr(cls, func.__name__).__doc__
+        func_docstring = func.__doc__
+        if func_docstring is None:
+            func.__doc__ = cls_docstring
+        else:
+            new_docstring = func_docstring % dict(super=cls_docstring)
+            func.__doc__ = new_docstring
+        return func
+    return _doc
+
+
+def extend_notes_in_docstring(cls, notes):
+    """
+    This decorator replaces the decorated function's docstring
+    with the docstring from corresponding method in `cls`.
+    It extends the 'Notes' section of that docstring to include
+    the given `notes`.
+    """
+    def _doc(func):
+        cls_docstring = getattr(cls, func.__name__).__doc__
+        # If python is called with -OO option,
+        # there is no docstring
+        if cls_docstring is None:
+            return func
+        end_of_notes = cls_docstring.find('        References\n')
+        if end_of_notes == -1:
+            end_of_notes = cls_docstring.find('        Examples\n')
+            if end_of_notes == -1:
+                end_of_notes = len(cls_docstring)
+        func.__doc__ = (cls_docstring[:end_of_notes] + notes +
+                        cls_docstring[end_of_notes:])
+        return func
+    return _doc
+
+
+def replace_notes_in_docstring(cls, notes):
+    """
+    This decorator replaces the decorated function's docstring
+    with the docstring from corresponding method in `cls`.
+    It replaces the 'Notes' section of that docstring with
+    the given `notes`.
+    """
+    def _doc(func):
+        cls_docstring = getattr(cls, func.__name__).__doc__
+        notes_header = '        Notes\n        -----\n'
+        # If python is called with -OO option,
+        # there is no docstring
+        if cls_docstring is None:
+            return func
+        start_of_notes = cls_docstring.find(notes_header)
+        end_of_notes = cls_docstring.find('        References\n')
+        if end_of_notes == -1:
+            end_of_notes = cls_docstring.find('        Examples\n')
+            if end_of_notes == -1:
+                end_of_notes = len(cls_docstring)
+        func.__doc__ = (cls_docstring[:start_of_notes + len(notes_header)] +
+                        notes +
+                        cls_docstring[end_of_notes:])
+        return func
+    return _doc
+
+
+def indentcount_lines(lines):
+    ''' Minimum indent for all lines in line list
+
+    >>> lines = [' one', '  two', '   three']
+    >>> indentcount_lines(lines)
+    1
+    >>> lines = []
+    >>> indentcount_lines(lines)
+    0
+    >>> lines = [' one']
+    >>> indentcount_lines(lines)
+    1
+    >>> indentcount_lines(['    '])
+    0
+    '''
+    indentno = sys.maxsize
+    for line in lines:
+        stripped = line.lstrip()
+        if stripped:
+            indentno = min(indentno, len(line) - len(stripped))
+    if indentno == sys.maxsize:
+        return 0
+    return indentno
+
+
+def filldoc(docdict, unindent_params=True):
+    ''' Return docstring decorator using docdict variable dictionary
+
+    Parameters
+    ----------
+    docdict : dictionary
+        dictionary containing name, docstring fragment pairs
+    unindent_params : {False, True}, boolean, optional
+        If True, strip common indentation from all parameters in
+        docdict
+
+    Returns
+    -------
+    decfunc : function
+        decorator that applies dictionary to input function docstring
+
+    '''
+    if unindent_params:
+        docdict = unindent_dict(docdict)
+
+    def decorate(f):
+        f.__doc__ = docformat(f.__doc__, docdict)
+        return f
+    return decorate
+
+
+def unindent_dict(docdict):
+    ''' Unindent all strings in a docdict '''
+    can_dict = {}
+    for name, dstr in docdict.items():
+        can_dict[name] = unindent_string(dstr)
+    return can_dict
+
+
+def unindent_string(docstring):
+    ''' Set docstring to minimum indent for all lines, including first
+
+    >>> unindent_string(' two')
+    'two'
+    >>> unindent_string('  two\\n   three')
+    'two\\n three'
+    '''
+    lines = docstring.expandtabs().splitlines()
+    icount = indentcount_lines(lines)
+    if icount == 0:
+        return docstring
+    return '\n'.join([line[icount:] for line in lines])
+
+
+def doc_replace(obj, oldval, newval):
+    """Decorator to take the docstring from obj, with oldval replaced by newval
+
+    Equivalent to ``func.__doc__ = obj.__doc__.replace(oldval, newval)``
+
+    Parameters
+    ----------
+    obj : object
+        The object to take the docstring from.
+    oldval : string
+        The string to replace from the original docstring.
+    newval : string
+        The string to replace ``oldval`` with.
+    """
+    # __doc__ may be None for optimized Python (-OO)
+    doc = (obj.__doc__ or '').replace(oldval, newval)
+
+    def inner(func):
+        func.__doc__ = doc
+        return func
+
+    return inner
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/messagestream.cpython-310-x86_64-linux-gnu.so b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/messagestream.cpython-310-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..72ce3c3e619b2215816e37ae8e27b6d9a9c2024b
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/messagestream.cpython-310-x86_64-linux-gnu.so differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__init__.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/__init__.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..21181aa01338cb2e4ac58e4271da900fbe5cfcf5
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/__init__.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__gcutils.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__gcutils.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..cebd9d13a9dde6a70335d874c796df4fc5b73283
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__gcutils.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__pep440.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__pep440.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..30054d27a32d53c76dd7a3c5afe3951042398ca3
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__pep440.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__testutils.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__testutils.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d1d61f2162753ad7ada056878dd0f81a58889809
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__testutils.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__threadsafety.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__threadsafety.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9b60f824a573a7a2593ea6c3d1b09475d671f458
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__threadsafety.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__util.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__util.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..54e0b6c6a1854ede70787e92c87b9d1537facf25
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test__util.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_array_api.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_array_api.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..93a735dbd4567ab3aebcd39604188d79d2149db0
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_array_api.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_bunch.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_bunch.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e81ae3ee5c1effc0cded8e1fd714b83de2d4200b
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_bunch.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_ccallback.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_ccallback.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b8750402c7eeb87446f32a0885b85b1bee99a869
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_ccallback.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_deprecation.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_deprecation.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..898e30bef5e43b745e6fc9b2847098495cae2968
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_deprecation.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_import_cycles.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_import_cycles.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7f10c35a32bd05fcc8e57a454d1edd9324f19cfe
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_import_cycles.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_public_api.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_public_api.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..6e2a9f6d8199faa0c1d582d032641ad1151e00f5
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_public_api.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_scipy_version.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_scipy_version.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f6f8eb11a4871af0154af23ecb95141dbb041e7d
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_scipy_version.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_tmpdirs.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_tmpdirs.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..82246525e6277441ae7e4023677d5060c5130ece
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_tmpdirs.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_warnings.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_warnings.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7b14e3b6e5e7f80c7bd93e57d8de0792774a9371
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/__pycache__/test_warnings.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__gcutils.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__gcutils.py
new file mode 100644
index 0000000000000000000000000000000000000000..74307fa0151cf1f6562acf4200e969f41ad2a006
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__gcutils.py
@@ -0,0 +1,101 @@
+""" Test for assert_deallocated context manager and gc utilities
+"""
+import gc
+
+from scipy._lib._gcutils import (set_gc_state, gc_state, assert_deallocated,
+                                 ReferenceError, IS_PYPY)
+
+from numpy.testing import assert_equal
+
+import pytest
+
+
+def test_set_gc_state():
+    gc_status = gc.isenabled()
+    try:
+        for state in (True, False):
+            gc.enable()
+            set_gc_state(state)
+            assert_equal(gc.isenabled(), state)
+            gc.disable()
+            set_gc_state(state)
+            assert_equal(gc.isenabled(), state)
+    finally:
+        if gc_status:
+            gc.enable()
+
+
+def test_gc_state():
+    # Test gc_state context manager
+    gc_status = gc.isenabled()
+    try:
+        for pre_state in (True, False):
+            set_gc_state(pre_state)
+            for with_state in (True, False):
+                # Check the gc state is with_state in with block
+                with gc_state(with_state):
+                    assert_equal(gc.isenabled(), with_state)
+                # And returns to previous state outside block
+                assert_equal(gc.isenabled(), pre_state)
+                # Even if the gc state is set explicitly within the block
+                with gc_state(with_state):
+                    assert_equal(gc.isenabled(), with_state)
+                    set_gc_state(not with_state)
+                assert_equal(gc.isenabled(), pre_state)
+    finally:
+        if gc_status:
+            gc.enable()
+
+
+@pytest.mark.skipif(IS_PYPY, reason="Test not meaningful on PyPy")
+def test_assert_deallocated():
+    # Ordinary use
+    class C:
+        def __init__(self, arg0, arg1, name='myname'):
+            self.name = name
+    for gc_current in (True, False):
+        with gc_state(gc_current):
+            # We are deleting from with-block context, so that's OK
+            with assert_deallocated(C, 0, 2, 'another name') as c:
+                assert_equal(c.name, 'another name')
+                del c
+            # Or not using the thing in with-block context, also OK
+            with assert_deallocated(C, 0, 2, name='third name'):
+                pass
+            assert_equal(gc.isenabled(), gc_current)
+
+
+@pytest.mark.skipif(IS_PYPY, reason="Test not meaningful on PyPy")
+def test_assert_deallocated_nodel():
+    class C:
+        pass
+    with pytest.raises(ReferenceError):
+        # Need to delete after using if in with-block context
+        # Note: assert_deallocated(C) needs to be assigned for the test
+        # to function correctly.  It is assigned to _, but _ itself is
+        # not referenced in the body of the with, it is only there for
+        # the refcount.
+        with assert_deallocated(C) as _:
+            pass
+
+
+@pytest.mark.skipif(IS_PYPY, reason="Test not meaningful on PyPy")
+def test_assert_deallocated_circular():
+    class C:
+        def __init__(self):
+            self._circular = self
+    with pytest.raises(ReferenceError):
+        # Circular reference, no automatic garbage collection
+        with assert_deallocated(C) as c:
+            del c
+
+
+@pytest.mark.skipif(IS_PYPY, reason="Test not meaningful on PyPy")
+def test_assert_deallocated_circular2():
+    class C:
+        def __init__(self):
+            self._circular = self
+    with pytest.raises(ReferenceError):
+        # Still circular reference, no automatic garbage collection
+        with assert_deallocated(C):
+            pass
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__pep440.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__pep440.py
new file mode 100644
index 0000000000000000000000000000000000000000..7f5b71c8f1e13b42de2e8e612a005dec409fc025
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__pep440.py
@@ -0,0 +1,67 @@
+from pytest import raises as assert_raises
+from scipy._lib._pep440 import Version, parse
+
+
+def test_main_versions():
+    assert Version('1.8.0') == Version('1.8.0')
+    for ver in ['1.9.0', '2.0.0', '1.8.1']:
+        assert Version('1.8.0') < Version(ver)
+
+    for ver in ['1.7.0', '1.7.1', '0.9.9']:
+        assert Version('1.8.0') > Version(ver)
+
+
+def test_version_1_point_10():
+    # regression test for gh-2998.
+    assert Version('1.9.0') < Version('1.10.0')
+    assert Version('1.11.0') < Version('1.11.1')
+    assert Version('1.11.0') == Version('1.11.0')
+    assert Version('1.99.11') < Version('1.99.12')
+
+
+def test_alpha_beta_rc():
+    assert Version('1.8.0rc1') == Version('1.8.0rc1')
+    for ver in ['1.8.0', '1.8.0rc2']:
+        assert Version('1.8.0rc1') < Version(ver)
+
+    for ver in ['1.8.0a2', '1.8.0b3', '1.7.2rc4']:
+        assert Version('1.8.0rc1') > Version(ver)
+
+    assert Version('1.8.0b1') > Version('1.8.0a2')
+
+
+def test_dev_version():
+    assert Version('1.9.0.dev+Unknown') < Version('1.9.0')
+    for ver in ['1.9.0', '1.9.0a1', '1.9.0b2', '1.9.0b2.dev+ffffffff', '1.9.0.dev1']:
+        assert Version('1.9.0.dev+f16acvda') < Version(ver)
+
+    assert Version('1.9.0.dev+f16acvda') == Version('1.9.0.dev+f16acvda')
+
+
+def test_dev_a_b_rc_mixed():
+    assert Version('1.9.0a2.dev+f16acvda') == Version('1.9.0a2.dev+f16acvda')
+    assert Version('1.9.0a2.dev+6acvda54') < Version('1.9.0a2')
+
+
+def test_dev0_version():
+    assert Version('1.9.0.dev0+Unknown') < Version('1.9.0')
+    for ver in ['1.9.0', '1.9.0a1', '1.9.0b2', '1.9.0b2.dev0+ffffffff']:
+        assert Version('1.9.0.dev0+f16acvda') < Version(ver)
+
+    assert Version('1.9.0.dev0+f16acvda') == Version('1.9.0.dev0+f16acvda')
+
+
+def test_dev0_a_b_rc_mixed():
+    assert Version('1.9.0a2.dev0+f16acvda') == Version('1.9.0a2.dev0+f16acvda')
+    assert Version('1.9.0a2.dev0+6acvda54') < Version('1.9.0a2')
+
+
+def test_raises():
+    for ver in ['1,9.0', '1.7.x']:
+        assert_raises(ValueError, Version, ver)
+
+def test_legacy_version():
+    # Non-PEP-440 version identifiers always compare less. For NumPy this only
+    # occurs on dev builds prior to 1.10.0 which are unsupported anyway.
+    assert parse('invalid') < Version('0.0.0')
+    assert parse('1.9.0-f16acvda') < Version('1.0.0')
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__testutils.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__testutils.py
new file mode 100644
index 0000000000000000000000000000000000000000..88db113d6d5a35c96ecc0a6a36ab42d74be49153
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__testutils.py
@@ -0,0 +1,32 @@
+import sys
+from scipy._lib._testutils import _parse_size, _get_mem_available
+import pytest
+
+
+def test__parse_size():
+    expected = {
+        '12': 12e6,
+        '12 b': 12,
+        '12k': 12e3,
+        '  12  M  ': 12e6,
+        '  12  G  ': 12e9,
+        ' 12Tb ': 12e12,
+        '12  Mib ': 12 * 1024.0**2,
+        '12Tib': 12 * 1024.0**4,
+    }
+
+    for inp, outp in sorted(expected.items()):
+        if outp is None:
+            with pytest.raises(ValueError):
+                _parse_size(inp)
+        else:
+            assert _parse_size(inp) == outp
+
+
+def test__mem_available():
+    # May return None on non-Linux platforms
+    available = _get_mem_available()
+    if sys.platform.startswith('linux'):
+        assert available >= 0
+    else:
+        assert available is None or available >= 0
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__threadsafety.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__threadsafety.py
new file mode 100644
index 0000000000000000000000000000000000000000..87ae85ef318da2b8bb104c4a87faa4e4021c01d5
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__threadsafety.py
@@ -0,0 +1,51 @@
+import threading
+import time
+import traceback
+
+from numpy.testing import assert_
+from pytest import raises as assert_raises
+
+from scipy._lib._threadsafety import ReentrancyLock, non_reentrant, ReentrancyError
+
+
+def test_parallel_threads():
+    # Check that ReentrancyLock serializes work in parallel threads.
+    #
+    # The test is not fully deterministic, and may succeed falsely if
+    # the timings go wrong.
+
+    lock = ReentrancyLock("failure")
+
+    failflag = [False]
+    exceptions_raised = []
+
+    def worker(k):
+        try:
+            with lock:
+                assert_(not failflag[0])
+                failflag[0] = True
+                time.sleep(0.1 * k)
+                assert_(failflag[0])
+                failflag[0] = False
+        except Exception:
+            exceptions_raised.append(traceback.format_exc(2))
+
+    threads = [threading.Thread(target=lambda k=k: worker(k))
+               for k in range(3)]
+    for t in threads:
+        t.start()
+    for t in threads:
+        t.join()
+
+    exceptions_raised = "\n".join(exceptions_raised)
+    assert_(not exceptions_raised, exceptions_raised)
+
+
+def test_reentering():
+    # Check that ReentrancyLock prevents re-entering from the same thread.
+
+    @non_reentrant()
+    def func(x):
+        return func(x)
+
+    assert_raises(ReentrancyError, func, 0)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__util.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__util.py
new file mode 100644
index 0000000000000000000000000000000000000000..b1f58acf3dc9fef524e4ff13270a8ade69a389e2
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test__util.py
@@ -0,0 +1,447 @@
+from multiprocessing import Pool
+from multiprocessing.pool import Pool as PWL
+import re
+import math
+from fractions import Fraction
+
+import numpy as np
+from numpy.testing import assert_equal, assert_
+import pytest
+from pytest import raises as assert_raises
+import hypothesis.extra.numpy as npst
+from hypothesis import given, strategies, reproduce_failure  # noqa: F401
+from scipy.conftest import array_api_compatible, skip_xp_invalid_arg
+
+from scipy._lib._array_api import (xp_assert_equal, xp_assert_close, is_numpy,
+                                   copy as xp_copy)
+from scipy._lib._util import (_aligned_zeros, check_random_state, MapWrapper,
+                              getfullargspec_no_self, FullArgSpec,
+                              rng_integers, _validate_int, _rename_parameter,
+                              _contains_nan, _rng_html_rewrite, _lazywhere)
+
+skip_xp_backends = pytest.mark.skip_xp_backends
+
+
+@pytest.mark.slow
+def test__aligned_zeros():
+    niter = 10
+
+    def check(shape, dtype, order, align):
+        err_msg = repr((shape, dtype, order, align))
+        x = _aligned_zeros(shape, dtype, order, align=align)
+        if align is None:
+            align = np.dtype(dtype).alignment
+        assert_equal(x.__array_interface__['data'][0] % align, 0)
+        if hasattr(shape, '__len__'):
+            assert_equal(x.shape, shape, err_msg)
+        else:
+            assert_equal(x.shape, (shape,), err_msg)
+        assert_equal(x.dtype, dtype)
+        if order == "C":
+            assert_(x.flags.c_contiguous, err_msg)
+        elif order == "F":
+            if x.size > 0:
+                # Size-0 arrays get invalid flags on NumPy 1.5
+                assert_(x.flags.f_contiguous, err_msg)
+        elif order is None:
+            assert_(x.flags.c_contiguous, err_msg)
+        else:
+            raise ValueError()
+
+    # try various alignments
+    for align in [1, 2, 3, 4, 8, 16, 32, 64, None]:
+        for n in [0, 1, 3, 11]:
+            for order in ["C", "F", None]:
+                for dtype in [np.uint8, np.float64]:
+                    for shape in [n, (1, 2, 3, n)]:
+                        for j in range(niter):
+                            check(shape, dtype, order, align)
+
+
+def test_check_random_state():
+    # If seed is None, return the RandomState singleton used by np.random.
+    # If seed is an int, return a new RandomState instance seeded with seed.
+    # If seed is already a RandomState instance, return it.
+    # Otherwise raise ValueError.
+    rsi = check_random_state(1)
+    assert_equal(type(rsi), np.random.RandomState)
+    rsi = check_random_state(rsi)
+    assert_equal(type(rsi), np.random.RandomState)
+    rsi = check_random_state(None)
+    assert_equal(type(rsi), np.random.RandomState)
+    assert_raises(ValueError, check_random_state, 'a')
+    rg = np.random.Generator(np.random.PCG64())
+    rsi = check_random_state(rg)
+    assert_equal(type(rsi), np.random.Generator)
+
+
+def test_getfullargspec_no_self():
+    p = MapWrapper(1)
+    argspec = getfullargspec_no_self(p.__init__)
+    assert_equal(argspec, FullArgSpec(['pool'], None, None, (1,), [],
+                                      None, {}))
+    argspec = getfullargspec_no_self(p.__call__)
+    assert_equal(argspec, FullArgSpec(['func', 'iterable'], None, None, None,
+                                      [], None, {}))
+
+    class _rv_generic:
+        def _rvs(self, a, b=2, c=3, *args, size=None, **kwargs):
+            return None
+
+    rv_obj = _rv_generic()
+    argspec = getfullargspec_no_self(rv_obj._rvs)
+    assert_equal(argspec, FullArgSpec(['a', 'b', 'c'], 'args', 'kwargs',
+                                      (2, 3), ['size'], {'size': None}, {}))
+
+
+def test_mapwrapper_serial():
+    in_arg = np.arange(10.)
+    out_arg = np.sin(in_arg)
+
+    p = MapWrapper(1)
+    assert_(p._mapfunc is map)
+    assert_(p.pool is None)
+    assert_(p._own_pool is False)
+    out = list(p(np.sin, in_arg))
+    assert_equal(out, out_arg)
+
+    with assert_raises(RuntimeError):
+        p = MapWrapper(0)
+
+
+def test_pool():
+    with Pool(2) as p:
+        p.map(math.sin, [1, 2, 3, 4])
+
+
+def test_mapwrapper_parallel():
+    in_arg = np.arange(10.)
+    out_arg = np.sin(in_arg)
+
+    with MapWrapper(2) as p:
+        out = p(np.sin, in_arg)
+        assert_equal(list(out), out_arg)
+
+        assert_(p._own_pool is True)
+        assert_(isinstance(p.pool, PWL))
+        assert_(p._mapfunc is not None)
+
+    # the context manager should've closed the internal pool
+    # check that it has by asking it to calculate again.
+    with assert_raises(Exception) as excinfo:
+        p(np.sin, in_arg)
+
+    assert_(excinfo.type is ValueError)
+
+    # can also set a PoolWrapper up with a map-like callable instance
+    with Pool(2) as p:
+        q = MapWrapper(p.map)
+
+        assert_(q._own_pool is False)
+        q.close()
+
+        # closing the PoolWrapper shouldn't close the internal pool
+        # because it didn't create it
+        out = p.map(np.sin, in_arg)
+        assert_equal(list(out), out_arg)
+
+
+def test_rng_integers():
+    rng = np.random.RandomState()
+
+    # test that numbers are inclusive of high point
+    arr = rng_integers(rng, low=2, high=5, size=100, endpoint=True)
+    assert np.max(arr) == 5
+    assert np.min(arr) == 2
+    assert arr.shape == (100, )
+
+    # test that numbers are inclusive of high point
+    arr = rng_integers(rng, low=5, size=100, endpoint=True)
+    assert np.max(arr) == 5
+    assert np.min(arr) == 0
+    assert arr.shape == (100, )
+
+    # test that numbers are exclusive of high point
+    arr = rng_integers(rng, low=2, high=5, size=100, endpoint=False)
+    assert np.max(arr) == 4
+    assert np.min(arr) == 2
+    assert arr.shape == (100, )
+
+    # test that numbers are exclusive of high point
+    arr = rng_integers(rng, low=5, size=100, endpoint=False)
+    assert np.max(arr) == 4
+    assert np.min(arr) == 0
+    assert arr.shape == (100, )
+
+    # now try with np.random.Generator
+    try:
+        rng = np.random.default_rng()
+    except AttributeError:
+        return
+
+    # test that numbers are inclusive of high point
+    arr = rng_integers(rng, low=2, high=5, size=100, endpoint=True)
+    assert np.max(arr) == 5
+    assert np.min(arr) == 2
+    assert arr.shape == (100, )
+
+    # test that numbers are inclusive of high point
+    arr = rng_integers(rng, low=5, size=100, endpoint=True)
+    assert np.max(arr) == 5
+    assert np.min(arr) == 0
+    assert arr.shape == (100, )
+
+    # test that numbers are exclusive of high point
+    arr = rng_integers(rng, low=2, high=5, size=100, endpoint=False)
+    assert np.max(arr) == 4
+    assert np.min(arr) == 2
+    assert arr.shape == (100, )
+
+    # test that numbers are exclusive of high point
+    arr = rng_integers(rng, low=5, size=100, endpoint=False)
+    assert np.max(arr) == 4
+    assert np.min(arr) == 0
+    assert arr.shape == (100, )
+
+
+class TestValidateInt:
+
+    @pytest.mark.parametrize('n', [4, np.uint8(4), np.int16(4), np.array(4)])
+    def test_validate_int(self, n):
+        n = _validate_int(n, 'n')
+        assert n == 4
+
+    @pytest.mark.parametrize('n', [4.0, np.array([4]), Fraction(4, 1)])
+    def test_validate_int_bad(self, n):
+        with pytest.raises(TypeError, match='n must be an integer'):
+            _validate_int(n, 'n')
+
+    def test_validate_int_below_min(self):
+        with pytest.raises(ValueError, match='n must be an integer not '
+                                             'less than 0'):
+            _validate_int(-1, 'n', 0)
+
+
+class TestRenameParameter:
+    # check that wrapper `_rename_parameter` for backward-compatible
+    # keyword renaming works correctly
+
+    # Example method/function that still accepts keyword `old`
+    @_rename_parameter("old", "new")
+    def old_keyword_still_accepted(self, new):
+        return new
+
+    # Example method/function for which keyword `old` is deprecated
+    @_rename_parameter("old", "new", dep_version="1.9.0")
+    def old_keyword_deprecated(self, new):
+        return new
+
+    def test_old_keyword_still_accepted(self):
+        # positional argument and both keyword work identically
+        res1 = self.old_keyword_still_accepted(10)
+        res2 = self.old_keyword_still_accepted(new=10)
+        res3 = self.old_keyword_still_accepted(old=10)
+        assert res1 == res2 == res3 == 10
+
+        # unexpected keyword raises an error
+        message = re.escape("old_keyword_still_accepted() got an unexpected")
+        with pytest.raises(TypeError, match=message):
+            self.old_keyword_still_accepted(unexpected=10)
+
+        # multiple values for the same parameter raises an error
+        message = re.escape("old_keyword_still_accepted() got multiple")
+        with pytest.raises(TypeError, match=message):
+            self.old_keyword_still_accepted(10, new=10)
+        with pytest.raises(TypeError, match=message):
+            self.old_keyword_still_accepted(10, old=10)
+        with pytest.raises(TypeError, match=message):
+            self.old_keyword_still_accepted(new=10, old=10)
+
+    def test_old_keyword_deprecated(self):
+        # positional argument and both keyword work identically,
+        # but use of old keyword results in DeprecationWarning
+        dep_msg = "Use of keyword argument `old` is deprecated"
+        res1 = self.old_keyword_deprecated(10)
+        res2 = self.old_keyword_deprecated(new=10)
+        with pytest.warns(DeprecationWarning, match=dep_msg):
+            res3 = self.old_keyword_deprecated(old=10)
+        assert res1 == res2 == res3 == 10
+
+        # unexpected keyword raises an error
+        message = re.escape("old_keyword_deprecated() got an unexpected")
+        with pytest.raises(TypeError, match=message):
+            self.old_keyword_deprecated(unexpected=10)
+
+        # multiple values for the same parameter raises an error and,
+        # if old keyword is used, results in DeprecationWarning
+        message = re.escape("old_keyword_deprecated() got multiple")
+        with pytest.raises(TypeError, match=message):
+            self.old_keyword_deprecated(10, new=10)
+        with pytest.raises(TypeError, match=message), \
+                pytest.warns(DeprecationWarning, match=dep_msg):
+            self.old_keyword_deprecated(10, old=10)
+        with pytest.raises(TypeError, match=message), \
+                pytest.warns(DeprecationWarning, match=dep_msg):
+            self.old_keyword_deprecated(new=10, old=10)
+
+
+class TestContainsNaNTest:
+
+    def test_policy(self):
+        data = np.array([1, 2, 3, np.nan])
+
+        contains_nan, nan_policy = _contains_nan(data, nan_policy="propagate")
+        assert contains_nan
+        assert nan_policy == "propagate"
+
+        contains_nan, nan_policy = _contains_nan(data, nan_policy="omit")
+        assert contains_nan
+        assert nan_policy == "omit"
+
+        msg = "The input contains nan values"
+        with pytest.raises(ValueError, match=msg):
+            _contains_nan(data, nan_policy="raise")
+
+        msg = "nan_policy must be one of"
+        with pytest.raises(ValueError, match=msg):
+            _contains_nan(data, nan_policy="nan")
+
+    def test_contains_nan(self):
+        data1 = np.array([1, 2, 3])
+        assert not _contains_nan(data1)[0]
+
+        data2 = np.array([1, 2, 3, np.nan])
+        assert _contains_nan(data2)[0]
+
+        data3 = np.array([np.nan, 2, 3, np.nan])
+        assert _contains_nan(data3)[0]
+
+        data4 = np.array([[1, 2], [3, 4]])
+        assert not _contains_nan(data4)[0]
+
+        data5 = np.array([[1, 2], [3, np.nan]])
+        assert _contains_nan(data5)[0]
+
+    @skip_xp_invalid_arg
+    def test_contains_nan_with_strings(self):
+        data1 = np.array([1, 2, "3", np.nan])  # converted to string "nan"
+        assert not _contains_nan(data1)[0]
+
+        data2 = np.array([1, 2, "3", np.nan], dtype='object')
+        assert _contains_nan(data2)[0]
+
+        data3 = np.array([["1", 2], [3, np.nan]])  # converted to string "nan"
+        assert not _contains_nan(data3)[0]
+
+        data4 = np.array([["1", 2], [3, np.nan]], dtype='object')
+        assert _contains_nan(data4)[0]
+
+    @skip_xp_backends('jax.numpy',
+                      reasons=["JAX arrays do not support item assignment"])
+    @pytest.mark.usefixtures("skip_xp_backends")
+    @array_api_compatible
+    @pytest.mark.parametrize("nan_policy", ['propagate', 'omit', 'raise'])
+    def test_array_api(self, xp, nan_policy):
+        rng = np.random.default_rng(932347235892482)
+        x0 = rng.random(size=(2, 3, 4))
+        x = xp.asarray(x0)
+        x_nan = xp_copy(x, xp=xp)
+        x_nan[1, 2, 1] = np.nan
+
+        contains_nan, nan_policy_out = _contains_nan(x, nan_policy=nan_policy)
+        assert not contains_nan
+        assert nan_policy_out == nan_policy
+
+        if nan_policy == 'raise':
+            message = 'The input contains...'
+            with pytest.raises(ValueError, match=message):
+                _contains_nan(x_nan, nan_policy=nan_policy)
+        elif nan_policy == 'omit' and not is_numpy(xp):
+            message = "`nan_policy='omit' is incompatible..."
+            with pytest.raises(ValueError, match=message):
+                _contains_nan(x_nan, nan_policy=nan_policy)
+        elif nan_policy == 'propagate':
+            contains_nan, nan_policy_out = _contains_nan(
+                x_nan, nan_policy=nan_policy)
+            assert contains_nan
+            assert nan_policy_out == nan_policy
+
+
+def test__rng_html_rewrite():
+    def mock_str():
+        lines = [
+            'np.random.default_rng(8989843)',
+            'np.random.default_rng(seed)',
+            'np.random.default_rng(0x9a71b21474694f919882289dc1559ca)',
+            ' bob ',
+        ]
+        return lines
+
+    res = _rng_html_rewrite(mock_str)()
+    ref = [
+        'np.random.default_rng()',
+        'np.random.default_rng(seed)',
+        'np.random.default_rng()',
+        ' bob ',
+    ]
+
+    assert res == ref
+
+
+class TestLazywhere:
+    n_arrays = strategies.integers(min_value=1, max_value=3)
+    rng_seed = strategies.integers(min_value=1000000000, max_value=9999999999)
+    dtype = strategies.sampled_from((np.float32, np.float64))
+    p = strategies.floats(min_value=0, max_value=1)
+    data = strategies.data()
+
+    @pytest.mark.fail_slow(5)
+    @pytest.mark.filterwarnings('ignore::RuntimeWarning')  # overflows, etc.
+    @skip_xp_backends('jax.numpy',
+                      reasons=["JAX arrays do not support item assignment"])
+    @pytest.mark.usefixtures("skip_xp_backends")
+    @array_api_compatible
+    @given(n_arrays=n_arrays, rng_seed=rng_seed, dtype=dtype, p=p, data=data)
+    def test_basic(self, n_arrays, rng_seed, dtype, p, data, xp):
+        mbs = npst.mutually_broadcastable_shapes(num_shapes=n_arrays+1,
+                                                 min_side=0)
+        input_shapes, result_shape = data.draw(mbs)
+        cond_shape, *shapes = input_shapes
+        fillvalue = xp.asarray(data.draw(npst.arrays(dtype=dtype, shape=tuple())))
+        arrays = [xp.asarray(data.draw(npst.arrays(dtype=dtype, shape=shape)))
+                  for shape in shapes]
+
+        def f(*args):
+            return sum(arg for arg in args)
+
+        def f2(*args):
+            return sum(arg for arg in args) / 2
+
+        rng = np.random.default_rng(rng_seed)
+        cond = xp.asarray(rng.random(size=cond_shape) > p)
+
+        res1 = _lazywhere(cond, arrays, f, fillvalue)
+        res2 = _lazywhere(cond, arrays, f, f2=f2)
+
+        # Ensure arrays are at least 1d to follow sane type promotion rules.
+        if xp == np:
+            cond, fillvalue, *arrays = np.atleast_1d(cond, fillvalue, *arrays)
+
+        ref1 = xp.where(cond, f(*arrays), fillvalue)
+        ref2 = xp.where(cond, f(*arrays), f2(*arrays))
+
+        if xp == np:
+            ref1 = ref1.reshape(result_shape)
+            ref2 = ref2.reshape(result_shape)
+            res1 = xp.asarray(res1)[()]
+            res2 = xp.asarray(res2)[()]
+
+        isinstance(res1, type(xp.asarray([])))
+        xp_assert_close(res1, ref1, rtol=2e-16)
+        assert_equal(res1.shape, ref1.shape)
+        assert_equal(res1.dtype, ref1.dtype)
+
+        isinstance(res2, type(xp.asarray([])))
+        xp_assert_equal(res2, ref2)
+        assert_equal(res2.shape, ref2.shape)
+        assert_equal(res2.dtype, ref2.dtype)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_array_api.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_array_api.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ca0ae36f03003e8e9e184157eba008e451382fd
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_array_api.py
@@ -0,0 +1,114 @@
+import numpy as np
+import pytest
+
+from scipy.conftest import array_api_compatible
+from scipy._lib._array_api import (
+    _GLOBAL_CONFIG, array_namespace, _asarray, copy, xp_assert_equal, is_numpy
+)
+import scipy._lib.array_api_compat.numpy as np_compat
+
+skip_xp_backends = pytest.mark.skip_xp_backends
+
+
+@pytest.mark.skipif(not _GLOBAL_CONFIG["SCIPY_ARRAY_API"],
+        reason="Array API test; set environment variable SCIPY_ARRAY_API=1 to run it")
+class TestArrayAPI:
+
+    def test_array_namespace(self):
+        x, y = np.array([0, 1, 2]), np.array([0, 1, 2])
+        xp = array_namespace(x, y)
+        assert 'array_api_compat.numpy' in xp.__name__
+
+        _GLOBAL_CONFIG["SCIPY_ARRAY_API"] = False
+        xp = array_namespace(x, y)
+        assert 'array_api_compat.numpy' in xp.__name__
+        _GLOBAL_CONFIG["SCIPY_ARRAY_API"] = True
+
+    @array_api_compatible
+    def test_asarray(self, xp):
+        x, y = _asarray([0, 1, 2], xp=xp), _asarray(np.arange(3), xp=xp)
+        ref = xp.asarray([0, 1, 2])
+        xp_assert_equal(x, ref)
+        xp_assert_equal(y, ref)
+
+    @pytest.mark.filterwarnings("ignore: the matrix subclass")
+    def test_raises(self):
+        msg = "of type `numpy.ma.MaskedArray` are not supported"
+        with pytest.raises(TypeError, match=msg):
+            array_namespace(np.ma.array(1), np.array(1))
+
+        msg = "of type `numpy.matrix` are not supported"
+        with pytest.raises(TypeError, match=msg):
+            array_namespace(np.array(1), np.matrix(1))
+
+        msg = "only boolean and numerical dtypes are supported"
+        with pytest.raises(TypeError, match=msg):
+            array_namespace([object()])
+        with pytest.raises(TypeError, match=msg):
+            array_namespace('abc')
+
+    def test_array_likes(self):
+        # should be no exceptions
+        array_namespace([0, 1, 2])
+        array_namespace(1, 2, 3)
+        array_namespace(1)
+
+    @skip_xp_backends('jax.numpy',
+                      reasons=["JAX arrays do not support item assignment"])
+    @pytest.mark.usefixtures("skip_xp_backends")
+    @array_api_compatible
+    def test_copy(self, xp):
+        for _xp in [xp, None]:
+            x = xp.asarray([1, 2, 3])
+            y = copy(x, xp=_xp)
+            # with numpy we'd want to use np.shared_memory, but that's not specified
+            # in the array-api
+            x[0] = 10
+            x[1] = 11
+            x[2] = 12
+
+            assert x[0] != y[0]
+            assert x[1] != y[1]
+            assert x[2] != y[2]
+            assert id(x) != id(y)
+
+    @array_api_compatible
+    @pytest.mark.parametrize('dtype', ['int32', 'int64', 'float32', 'float64'])
+    @pytest.mark.parametrize('shape', [(), (3,)])
+    def test_strict_checks(self, xp, dtype, shape):
+        # Check that `_strict_check` behaves as expected
+        dtype = getattr(xp, dtype)
+        x = xp.broadcast_to(xp.asarray(1, dtype=dtype), shape)
+        x = x if shape else x[()]
+        y = np_compat.asarray(1)[()]
+
+        options = dict(check_namespace=True, check_dtype=False, check_shape=False)
+        if xp == np:
+            xp_assert_equal(x, y, **options)
+        else:
+            with pytest.raises(AssertionError, match="Namespaces do not match."):
+                xp_assert_equal(x, y, **options)
+
+        options = dict(check_namespace=False, check_dtype=True, check_shape=False)
+        if y.dtype.name in str(x.dtype):
+            xp_assert_equal(x, y, **options)
+        else:
+            with pytest.raises(AssertionError, match="dtypes do not match."):
+                xp_assert_equal(x, y, **options)
+
+        options = dict(check_namespace=False, check_dtype=False, check_shape=True)
+        if x.shape == y.shape:
+            xp_assert_equal(x, y, **options)
+        else:
+            with pytest.raises(AssertionError, match="Shapes do not match."):
+                xp_assert_equal(x, y, **options)
+
+    @array_api_compatible
+    def test_check_scalar(self, xp):
+        if not is_numpy(xp):
+            pytest.skip("Scalars only exist in NumPy")
+
+        if is_numpy(xp):
+            with pytest.raises(AssertionError, match="Types do not match."):
+                xp_assert_equal(xp.asarray(0.), xp.float64(0))
+            xp_assert_equal(xp.float64(0), xp.asarray(0.))
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_bunch.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_bunch.py
new file mode 100644
index 0000000000000000000000000000000000000000..f19ca377129b925cad732dd25bf3089c646f923f
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_bunch.py
@@ -0,0 +1,162 @@
+import pytest
+import pickle
+from numpy.testing import assert_equal
+from scipy._lib._bunch import _make_tuple_bunch
+
+
+# `Result` is defined at the top level of the module so it can be
+# used to test pickling.
+Result = _make_tuple_bunch('Result', ['x', 'y', 'z'], ['w', 'beta'])
+
+
+class TestMakeTupleBunch:
+
+    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # Tests with Result
+    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    def setup_method(self):
+        # Set up an instance of Result.
+        self.result = Result(x=1, y=2, z=3, w=99, beta=0.5)
+
+    def test_attribute_access(self):
+        assert_equal(self.result.x, 1)
+        assert_equal(self.result.y, 2)
+        assert_equal(self.result.z, 3)
+        assert_equal(self.result.w, 99)
+        assert_equal(self.result.beta, 0.5)
+
+    def test_indexing(self):
+        assert_equal(self.result[0], 1)
+        assert_equal(self.result[1], 2)
+        assert_equal(self.result[2], 3)
+        assert_equal(self.result[-1], 3)
+        with pytest.raises(IndexError, match='index out of range'):
+            self.result[3]
+
+    def test_unpacking(self):
+        x0, y0, z0 = self.result
+        assert_equal((x0, y0, z0), (1, 2, 3))
+        assert_equal(self.result, (1, 2, 3))
+
+    def test_slice(self):
+        assert_equal(self.result[1:], (2, 3))
+        assert_equal(self.result[::2], (1, 3))
+        assert_equal(self.result[::-1], (3, 2, 1))
+
+    def test_len(self):
+        assert_equal(len(self.result), 3)
+
+    def test_repr(self):
+        s = repr(self.result)
+        assert_equal(s, 'Result(x=1, y=2, z=3, w=99, beta=0.5)')
+
+    def test_hash(self):
+        assert_equal(hash(self.result), hash((1, 2, 3)))
+
+    def test_pickle(self):
+        s = pickle.dumps(self.result)
+        obj = pickle.loads(s)
+        assert isinstance(obj, Result)
+        assert_equal(obj.x, self.result.x)
+        assert_equal(obj.y, self.result.y)
+        assert_equal(obj.z, self.result.z)
+        assert_equal(obj.w, self.result.w)
+        assert_equal(obj.beta, self.result.beta)
+
+    def test_read_only_existing(self):
+        with pytest.raises(AttributeError, match="can't set attribute"):
+            self.result.x = -1
+
+    def test_read_only_new(self):
+        self.result.plate_of_shrimp = "lattice of coincidence"
+        assert self.result.plate_of_shrimp == "lattice of coincidence"
+
+    def test_constructor_missing_parameter(self):
+        with pytest.raises(TypeError, match='missing'):
+            # `w` is missing.
+            Result(x=1, y=2, z=3, beta=0.75)
+
+    def test_constructor_incorrect_parameter(self):
+        with pytest.raises(TypeError, match='unexpected'):
+            # `foo` is not an existing field.
+            Result(x=1, y=2, z=3, w=123, beta=0.75, foo=999)
+
+    def test_module(self):
+        m = 'scipy._lib.tests.test_bunch'
+        assert_equal(Result.__module__, m)
+        assert_equal(self.result.__module__, m)
+
+    def test_extra_fields_per_instance(self):
+        # This test exists to ensure that instances of the same class
+        # store their own values for the extra fields. That is, the values
+        # are stored per instance and not in the class.
+        result1 = Result(x=1, y=2, z=3, w=-1, beta=0.0)
+        result2 = Result(x=4, y=5, z=6, w=99, beta=1.0)
+        assert_equal(result1.w, -1)
+        assert_equal(result1.beta, 0.0)
+        # The rest of these checks aren't essential, but let's check
+        # them anyway.
+        assert_equal(result1[:], (1, 2, 3))
+        assert_equal(result2.w, 99)
+        assert_equal(result2.beta, 1.0)
+        assert_equal(result2[:], (4, 5, 6))
+
+    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # Other tests
+    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    def test_extra_field_names_is_optional(self):
+        Square = _make_tuple_bunch('Square', ['width', 'height'])
+        sq = Square(width=1, height=2)
+        assert_equal(sq.width, 1)
+        assert_equal(sq.height, 2)
+        s = repr(sq)
+        assert_equal(s, 'Square(width=1, height=2)')
+
+    def test_tuple_like(self):
+        Tup = _make_tuple_bunch('Tup', ['a', 'b'])
+        tu = Tup(a=1, b=2)
+        assert isinstance(tu, tuple)
+        assert isinstance(tu + (1,), tuple)
+
+    def test_explicit_module(self):
+        m = 'some.module.name'
+        Foo = _make_tuple_bunch('Foo', ['x'], ['a', 'b'], module=m)
+        foo = Foo(x=1, a=355, b=113)
+        assert_equal(Foo.__module__, m)
+        assert_equal(foo.__module__, m)
+
+    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # Argument validation
+    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    @pytest.mark.parametrize('args', [('123', ['a'], ['b']),
+                                      ('Foo', ['-3'], ['x']),
+                                      ('Foo', ['a'], ['+-*/'])])
+    def test_identifiers_not_allowed(self, args):
+        with pytest.raises(ValueError, match='identifiers'):
+            _make_tuple_bunch(*args)
+
+    @pytest.mark.parametrize('args', [('Foo', ['a', 'b', 'a'], ['x']),
+                                      ('Foo', ['a', 'b'], ['b', 'x'])])
+    def test_repeated_field_names(self, args):
+        with pytest.raises(ValueError, match='Duplicate'):
+            _make_tuple_bunch(*args)
+
+    @pytest.mark.parametrize('args', [('Foo', ['_a'], ['x']),
+                                      ('Foo', ['a'], ['_x'])])
+    def test_leading_underscore_not_allowed(self, args):
+        with pytest.raises(ValueError, match='underscore'):
+            _make_tuple_bunch(*args)
+
+    @pytest.mark.parametrize('args', [('Foo', ['def'], ['x']),
+                                      ('Foo', ['a'], ['or']),
+                                      ('and', ['a'], ['x'])])
+    def test_keyword_not_allowed_in_fields(self, args):
+        with pytest.raises(ValueError, match='keyword'):
+            _make_tuple_bunch(*args)
+
+    def test_at_least_one_field_name_required(self):
+        with pytest.raises(ValueError, match='at least one name'):
+            _make_tuple_bunch('Qwerty', [], ['a', 'b'])
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_ccallback.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_ccallback.py
new file mode 100644
index 0000000000000000000000000000000000000000..82021775c294c7b881b9458b57d16deaac483cc7
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_ccallback.py
@@ -0,0 +1,204 @@
+from numpy.testing import assert_equal, assert_
+from pytest import raises as assert_raises
+
+import time
+import pytest
+import ctypes
+import threading
+from scipy._lib import _ccallback_c as _test_ccallback_cython
+from scipy._lib import _test_ccallback
+from scipy._lib._ccallback import LowLevelCallable
+
+try:
+    import cffi
+    HAVE_CFFI = True
+except ImportError:
+    HAVE_CFFI = False
+
+
+ERROR_VALUE = 2.0
+
+
+def callback_python(a, user_data=None):
+    if a == ERROR_VALUE:
+        raise ValueError("bad value")
+
+    if user_data is None:
+        return a + 1
+    else:
+        return a + user_data
+
+def _get_cffi_func(base, signature):
+    if not HAVE_CFFI:
+        pytest.skip("cffi not installed")
+
+    # Get function address
+    voidp = ctypes.cast(base, ctypes.c_void_p)
+    address = voidp.value
+
+    # Create corresponding cffi handle
+    ffi = cffi.FFI()
+    func = ffi.cast(signature, address)
+    return func
+
+
+def _get_ctypes_data():
+    value = ctypes.c_double(2.0)
+    return ctypes.cast(ctypes.pointer(value), ctypes.c_voidp)
+
+
+def _get_cffi_data():
+    if not HAVE_CFFI:
+        pytest.skip("cffi not installed")
+    ffi = cffi.FFI()
+    return ffi.new('double *', 2.0)
+
+
+CALLERS = {
+    'simple': _test_ccallback.test_call_simple,
+    'nodata': _test_ccallback.test_call_nodata,
+    'nonlocal': _test_ccallback.test_call_nonlocal,
+    'cython': _test_ccallback_cython.test_call_cython,
+}
+
+# These functions have signatures known to the callers
+FUNCS = {
+    'python': lambda: callback_python,
+    'capsule': lambda: _test_ccallback.test_get_plus1_capsule(),
+    'cython': lambda: LowLevelCallable.from_cython(_test_ccallback_cython,
+                                                   "plus1_cython"),
+    'ctypes': lambda: _test_ccallback_cython.plus1_ctypes,
+    'cffi': lambda: _get_cffi_func(_test_ccallback_cython.plus1_ctypes,
+                                   'double (*)(double, int *, void *)'),
+    'capsule_b': lambda: _test_ccallback.test_get_plus1b_capsule(),
+    'cython_b': lambda: LowLevelCallable.from_cython(_test_ccallback_cython,
+                                                     "plus1b_cython"),
+    'ctypes_b': lambda: _test_ccallback_cython.plus1b_ctypes,
+    'cffi_b': lambda: _get_cffi_func(_test_ccallback_cython.plus1b_ctypes,
+                                     'double (*)(double, double, int *, void *)'),
+}
+
+# These functions have signatures the callers don't know
+BAD_FUNCS = {
+    'capsule_bc': lambda: _test_ccallback.test_get_plus1bc_capsule(),
+    'cython_bc': lambda: LowLevelCallable.from_cython(_test_ccallback_cython,
+                                                      "plus1bc_cython"),
+    'ctypes_bc': lambda: _test_ccallback_cython.plus1bc_ctypes,
+    'cffi_bc': lambda: _get_cffi_func(
+        _test_ccallback_cython.plus1bc_ctypes,
+        'double (*)(double, double, double, int *, void *)'
+    ),
+}
+
+USER_DATAS = {
+    'ctypes': _get_ctypes_data,
+    'cffi': _get_cffi_data,
+    'capsule': _test_ccallback.test_get_data_capsule,
+}
+
+
+def test_callbacks():
+    def check(caller, func, user_data):
+        caller = CALLERS[caller]
+        func = FUNCS[func]()
+        user_data = USER_DATAS[user_data]()
+
+        if func is callback_python:
+            def func2(x):
+                return func(x, 2.0)
+        else:
+            func2 = LowLevelCallable(func, user_data)
+            func = LowLevelCallable(func)
+
+        # Test basic call
+        assert_equal(caller(func, 1.0), 2.0)
+
+        # Test 'bad' value resulting to an error
+        assert_raises(ValueError, caller, func, ERROR_VALUE)
+
+        # Test passing in user_data
+        assert_equal(caller(func2, 1.0), 3.0)
+
+    for caller in sorted(CALLERS.keys()):
+        for func in sorted(FUNCS.keys()):
+            for user_data in sorted(USER_DATAS.keys()):
+                check(caller, func, user_data)
+
+
+def test_bad_callbacks():
+    def check(caller, func, user_data):
+        caller = CALLERS[caller]
+        user_data = USER_DATAS[user_data]()
+        func = BAD_FUNCS[func]()
+
+        if func is callback_python:
+            def func2(x):
+                return func(x, 2.0)
+        else:
+            func2 = LowLevelCallable(func, user_data)
+            func = LowLevelCallable(func)
+
+        # Test that basic call fails
+        assert_raises(ValueError, caller, LowLevelCallable(func), 1.0)
+
+        # Test that passing in user_data also fails
+        assert_raises(ValueError, caller, func2, 1.0)
+
+        # Test error message
+        llfunc = LowLevelCallable(func)
+        try:
+            caller(llfunc, 1.0)
+        except ValueError as err:
+            msg = str(err)
+            assert_(llfunc.signature in msg, msg)
+            assert_('double (double, double, int *, void *)' in msg, msg)
+
+    for caller in sorted(CALLERS.keys()):
+        for func in sorted(BAD_FUNCS.keys()):
+            for user_data in sorted(USER_DATAS.keys()):
+                check(caller, func, user_data)
+
+
+def test_signature_override():
+    caller = _test_ccallback.test_call_simple
+    func = _test_ccallback.test_get_plus1_capsule()
+
+    llcallable = LowLevelCallable(func, signature="bad signature")
+    assert_equal(llcallable.signature, "bad signature")
+    assert_raises(ValueError, caller, llcallable, 3)
+
+    llcallable = LowLevelCallable(func, signature="double (double, int *, void *)")
+    assert_equal(llcallable.signature, "double (double, int *, void *)")
+    assert_equal(caller(llcallable, 3), 4)
+
+
+def test_threadsafety():
+    def callback(a, caller):
+        if a <= 0:
+            return 1
+        else:
+            res = caller(lambda x: callback(x, caller), a - 1)
+            return 2*res
+
+    def check(caller):
+        caller = CALLERS[caller]
+
+        results = []
+
+        count = 10
+
+        def run():
+            time.sleep(0.01)
+            r = caller(lambda x: callback(x, caller), count)
+            results.append(r)
+
+        threads = [threading.Thread(target=run) for j in range(20)]
+        for thread in threads:
+            thread.start()
+        for thread in threads:
+            thread.join()
+
+        assert_equal(results, [2.0**count]*len(threads))
+
+    for caller in CALLERS.keys():
+        check(caller)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_deprecation.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_deprecation.py
new file mode 100644
index 0000000000000000000000000000000000000000..7910bd56f6b0c37276c9dff5a15cd3ddf755840e
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_deprecation.py
@@ -0,0 +1,10 @@
+import pytest
+
+
+def test_cython_api_deprecation():
+    match = ("`scipy._lib._test_deprecation_def.foo_deprecated` "
+             "is deprecated, use `foo` instead!\n"
+             "Deprecated in Scipy 42.0.0")
+    with pytest.warns(DeprecationWarning, match=match):
+        from .. import _test_deprecation_call
+    assert _test_deprecation_call.call() == (1, 1)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_import_cycles.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_import_cycles.py
new file mode 100644
index 0000000000000000000000000000000000000000..02177fec255ef514133466e7de701d3e7bf6fa1d
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_import_cycles.py
@@ -0,0 +1,17 @@
+import pytest
+import sys
+import subprocess
+
+from .test_public_api import PUBLIC_MODULES
+
+# Regression tests for gh-6793.
+# Check that all modules are importable in a new Python process.
+# This is not necessarily true if there are import cycles present.
+
+@pytest.mark.fail_slow(20)
+@pytest.mark.slow
+def test_public_modules_importable():
+    pids = [subprocess.Popen([sys.executable, '-c', f'import {module}'])
+            for module in PUBLIC_MODULES]
+    for i, pid in enumerate(pids):
+        assert pid.wait() == 0, f'Failed to import {PUBLIC_MODULES[i]}'
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_public_api.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_public_api.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e789f9ccb33f3f78ea5d61a8a46fdc26a57e367
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_public_api.py
@@ -0,0 +1,496 @@
+"""
+This test script is adopted from:
+    https://github.com/numpy/numpy/blob/main/numpy/tests/test_public_api.py
+"""
+
+import pkgutil
+import types
+import importlib
+import warnings
+from importlib import import_module
+
+import pytest
+
+import scipy
+
+from scipy.conftest import xp_available_backends
+
+
+def test_dir_testing():
+    """Assert that output of dir has only one "testing/tester"
+    attribute without duplicate"""
+    assert len(dir(scipy)) == len(set(dir(scipy)))
+
+
+# Historically SciPy has not used leading underscores for private submodules
+# much.  This has resulted in lots of things that look like public modules
+# (i.e. things that can be imported as `import scipy.somesubmodule.somefile`),
+# but were never intended to be public.  The PUBLIC_MODULES list contains
+# modules that are either public because they were meant to be, or because they
+# contain public functions/objects that aren't present in any other namespace
+# for whatever reason and therefore should be treated as public.
+PUBLIC_MODULES = ["scipy." + s for s in [
+    "cluster",
+    "cluster.vq",
+    "cluster.hierarchy",
+    "constants",
+    "datasets",
+    "fft",
+    "fftpack",
+    "integrate",
+    "interpolate",
+    "io",
+    "io.arff",
+    "io.matlab",
+    "io.wavfile",
+    "linalg",
+    "linalg.blas",
+    "linalg.cython_blas",
+    "linalg.lapack",
+    "linalg.cython_lapack",
+    "linalg.interpolative",
+    "misc",
+    "ndimage",
+    "odr",
+    "optimize",
+    "signal",
+    "signal.windows",
+    "sparse",
+    "sparse.linalg",
+    "sparse.csgraph",
+    "spatial",
+    "spatial.distance",
+    "spatial.transform",
+    "special",
+    "stats",
+    "stats.contingency",
+    "stats.distributions",
+    "stats.mstats",
+    "stats.qmc",
+    "stats.sampling"
+]]
+
+# The PRIVATE_BUT_PRESENT_MODULES list contains modules that lacked underscores
+# in their name and hence looked public, but weren't meant to be. All these
+# namespace were deprecated in the 1.8.0 release - see "clear split between
+# public and private API" in the 1.8.0 release notes.
+# These private modules support will be removed in SciPy v2.0.0, as the
+# deprecation messages emitted by each of these modules say.
+PRIVATE_BUT_PRESENT_MODULES = [
+    'scipy.constants.codata',
+    'scipy.constants.constants',
+    'scipy.fftpack.basic',
+    'scipy.fftpack.convolve',
+    'scipy.fftpack.helper',
+    'scipy.fftpack.pseudo_diffs',
+    'scipy.fftpack.realtransforms',
+    'scipy.integrate.dop',
+    'scipy.integrate.lsoda',
+    'scipy.integrate.odepack',
+    'scipy.integrate.quadpack',
+    'scipy.integrate.vode',
+    'scipy.interpolate.dfitpack',
+    'scipy.interpolate.fitpack',
+    'scipy.interpolate.fitpack2',
+    'scipy.interpolate.interpnd',
+    'scipy.interpolate.interpolate',
+    'scipy.interpolate.ndgriddata',
+    'scipy.interpolate.polyint',
+    'scipy.interpolate.rbf',
+    'scipy.io.arff.arffread',
+    'scipy.io.harwell_boeing',
+    'scipy.io.idl',
+    'scipy.io.matlab.byteordercodes',
+    'scipy.io.matlab.mio',
+    'scipy.io.matlab.mio4',
+    'scipy.io.matlab.mio5',
+    'scipy.io.matlab.mio5_params',
+    'scipy.io.matlab.mio5_utils',
+    'scipy.io.matlab.mio_utils',
+    'scipy.io.matlab.miobase',
+    'scipy.io.matlab.streams',
+    'scipy.io.mmio',
+    'scipy.io.netcdf',
+    'scipy.linalg.basic',
+    'scipy.linalg.decomp',
+    'scipy.linalg.decomp_cholesky',
+    'scipy.linalg.decomp_lu',
+    'scipy.linalg.decomp_qr',
+    'scipy.linalg.decomp_schur',
+    'scipy.linalg.decomp_svd',
+    'scipy.linalg.matfuncs',
+    'scipy.linalg.misc',
+    'scipy.linalg.special_matrices',
+    'scipy.misc.common',
+    'scipy.misc.doccer',
+    'scipy.ndimage.filters',
+    'scipy.ndimage.fourier',
+    'scipy.ndimage.interpolation',
+    'scipy.ndimage.measurements',
+    'scipy.ndimage.morphology',
+    'scipy.odr.models',
+    'scipy.odr.odrpack',
+    'scipy.optimize.cobyla',
+    'scipy.optimize.cython_optimize',
+    'scipy.optimize.lbfgsb',
+    'scipy.optimize.linesearch',
+    'scipy.optimize.minpack',
+    'scipy.optimize.minpack2',
+    'scipy.optimize.moduleTNC',
+    'scipy.optimize.nonlin',
+    'scipy.optimize.optimize',
+    'scipy.optimize.slsqp',
+    'scipy.optimize.tnc',
+    'scipy.optimize.zeros',
+    'scipy.signal.bsplines',
+    'scipy.signal.filter_design',
+    'scipy.signal.fir_filter_design',
+    'scipy.signal.lti_conversion',
+    'scipy.signal.ltisys',
+    'scipy.signal.signaltools',
+    'scipy.signal.spectral',
+    'scipy.signal.spline',
+    'scipy.signal.waveforms',
+    'scipy.signal.wavelets',
+    'scipy.signal.windows.windows',
+    'scipy.sparse.base',
+    'scipy.sparse.bsr',
+    'scipy.sparse.compressed',
+    'scipy.sparse.construct',
+    'scipy.sparse.coo',
+    'scipy.sparse.csc',
+    'scipy.sparse.csr',
+    'scipy.sparse.data',
+    'scipy.sparse.dia',
+    'scipy.sparse.dok',
+    'scipy.sparse.extract',
+    'scipy.sparse.lil',
+    'scipy.sparse.linalg.dsolve',
+    'scipy.sparse.linalg.eigen',
+    'scipy.sparse.linalg.interface',
+    'scipy.sparse.linalg.isolve',
+    'scipy.sparse.linalg.matfuncs',
+    'scipy.sparse.sparsetools',
+    'scipy.sparse.spfuncs',
+    'scipy.sparse.sputils',
+    'scipy.spatial.ckdtree',
+    'scipy.spatial.kdtree',
+    'scipy.spatial.qhull',
+    'scipy.spatial.transform.rotation',
+    'scipy.special.add_newdocs',
+    'scipy.special.basic',
+    'scipy.special.cython_special',
+    'scipy.special.orthogonal',
+    'scipy.special.sf_error',
+    'scipy.special.specfun',
+    'scipy.special.spfun_stats',
+    'scipy.stats.biasedurn',
+    'scipy.stats.kde',
+    'scipy.stats.morestats',
+    'scipy.stats.mstats_basic',
+    'scipy.stats.mstats_extras',
+    'scipy.stats.mvn',
+    'scipy.stats.stats',
+]
+
+
+def is_unexpected(name):
+    """Check if this needs to be considered."""
+    if '._' in name or '.tests' in name or '.setup' in name:
+        return False
+
+    if name in PUBLIC_MODULES:
+        return False
+
+    if name in PRIVATE_BUT_PRESENT_MODULES:
+        return False
+
+    return True
+
+
+SKIP_LIST = [
+    'scipy.conftest',
+    'scipy.version',
+    'scipy.special.libsf_error_state'
+]
+
+
+# XXX: this test does more than it says on the tin - in using `pkgutil.walk_packages`,
+# it will raise if it encounters any exceptions which are not handled by `ignore_errors`
+# while attempting to import each discovered package.
+# For now, `ignore_errors` only ignores what is necessary, but this could be expanded -
+# for example, to all errors from private modules or git subpackages - if desired.
+def test_all_modules_are_expected():
+    """
+    Test that we don't add anything that looks like a new public module by
+    accident.  Check is based on filenames.
+    """
+
+    def ignore_errors(name):
+        # if versions of other array libraries are installed which are incompatible
+        # with the installed NumPy version, there can be errors on importing
+        # `array_api_compat`. This should only raise if SciPy is configured with
+        # that library as an available backend.
+        backends = {'cupy': 'cupy',
+                    'pytorch': 'torch',
+                    'dask.array': 'dask.array'}
+        for backend, dir_name in backends.items():
+            path = f'array_api_compat.{dir_name}'
+            if path in name and backend not in xp_available_backends:
+                return
+        raise
+
+    modnames = []
+
+    for _, modname, _ in pkgutil.walk_packages(path=scipy.__path__,
+                                               prefix=scipy.__name__ + '.',
+                                               onerror=ignore_errors):
+        if is_unexpected(modname) and modname not in SKIP_LIST:
+            # We have a name that is new.  If that's on purpose, add it to
+            # PUBLIC_MODULES.  We don't expect to have to add anything to
+            # PRIVATE_BUT_PRESENT_MODULES.  Use an underscore in the name!
+            modnames.append(modname)
+
+    if modnames:
+        raise AssertionError(f'Found unexpected modules: {modnames}')
+
+
+# Stuff that clearly shouldn't be in the API and is detected by the next test
+# below
+SKIP_LIST_2 = [
+    'scipy.char',
+    'scipy.rec',
+    'scipy.emath',
+    'scipy.math',
+    'scipy.random',
+    'scipy.ctypeslib',
+    'scipy.ma'
+]
+
+
+def test_all_modules_are_expected_2():
+    """
+    Method checking all objects. The pkgutil-based method in
+    `test_all_modules_are_expected` does not catch imports into a namespace,
+    only filenames.
+    """
+
+    def find_unexpected_members(mod_name):
+        members = []
+        module = importlib.import_module(mod_name)
+        if hasattr(module, '__all__'):
+            objnames = module.__all__
+        else:
+            objnames = dir(module)
+
+        for objname in objnames:
+            if not objname.startswith('_'):
+                fullobjname = mod_name + '.' + objname
+                if isinstance(getattr(module, objname), types.ModuleType):
+                    if is_unexpected(fullobjname) and fullobjname not in SKIP_LIST_2:
+                        members.append(fullobjname)
+
+        return members
+
+    unexpected_members = find_unexpected_members("scipy")
+    for modname in PUBLIC_MODULES:
+        unexpected_members.extend(find_unexpected_members(modname))
+
+    if unexpected_members:
+        raise AssertionError("Found unexpected object(s) that look like "
+                             f"modules: {unexpected_members}")
+
+
+def test_api_importable():
+    """
+    Check that all submodules listed higher up in this file can be imported
+    Note that if a PRIVATE_BUT_PRESENT_MODULES entry goes missing, it may
+    simply need to be removed from the list (deprecation may or may not be
+    needed - apply common sense).
+    """
+    def check_importable(module_name):
+        try:
+            importlib.import_module(module_name)
+        except (ImportError, AttributeError):
+            return False
+
+        return True
+
+    module_names = []
+    for module_name in PUBLIC_MODULES:
+        if not check_importable(module_name):
+            module_names.append(module_name)
+
+    if module_names:
+        raise AssertionError("Modules in the public API that cannot be "
+                             f"imported: {module_names}")
+
+    with warnings.catch_warnings(record=True):
+        warnings.filterwarnings('always', category=DeprecationWarning)
+        warnings.filterwarnings('always', category=ImportWarning)
+        for module_name in PRIVATE_BUT_PRESENT_MODULES:
+            if not check_importable(module_name):
+                module_names.append(module_name)
+
+    if module_names:
+        raise AssertionError("Modules that are not really public but looked "
+                             "public and can not be imported: "
+                             f"{module_names}")
+
+
+@pytest.mark.parametrize(("module_name", "correct_module"),
+                         [('scipy.constants.codata', None),
+                          ('scipy.constants.constants', None),
+                          ('scipy.fftpack.basic', None),
+                          ('scipy.fftpack.helper', None),
+                          ('scipy.fftpack.pseudo_diffs', None),
+                          ('scipy.fftpack.realtransforms', None),
+                          ('scipy.integrate.dop', None),
+                          ('scipy.integrate.lsoda', None),
+                          ('scipy.integrate.odepack', None),
+                          ('scipy.integrate.quadpack', None),
+                          ('scipy.integrate.vode', None),
+                          ('scipy.interpolate.fitpack', None),
+                          ('scipy.interpolate.fitpack2', None),
+                          ('scipy.interpolate.interpolate', None),
+                          ('scipy.interpolate.ndgriddata', None),
+                          ('scipy.interpolate.polyint', None),
+                          ('scipy.interpolate.rbf', None),
+                          ('scipy.io.harwell_boeing', None),
+                          ('scipy.io.idl', None),
+                          ('scipy.io.mmio', None),
+                          ('scipy.io.netcdf', None),
+                          ('scipy.io.arff.arffread', 'arff'),
+                          ('scipy.io.matlab.byteordercodes', 'matlab'),
+                          ('scipy.io.matlab.mio_utils', 'matlab'),
+                          ('scipy.io.matlab.mio', 'matlab'),
+                          ('scipy.io.matlab.mio4', 'matlab'),
+                          ('scipy.io.matlab.mio5_params', 'matlab'),
+                          ('scipy.io.matlab.mio5_utils', 'matlab'),
+                          ('scipy.io.matlab.mio5', 'matlab'),
+                          ('scipy.io.matlab.miobase', 'matlab'),
+                          ('scipy.io.matlab.streams', 'matlab'),
+                          ('scipy.linalg.basic', None),
+                          ('scipy.linalg.decomp', None),
+                          ('scipy.linalg.decomp_cholesky', None),
+                          ('scipy.linalg.decomp_lu', None),
+                          ('scipy.linalg.decomp_qr', None),
+                          ('scipy.linalg.decomp_schur', None),
+                          ('scipy.linalg.decomp_svd', None),
+                          ('scipy.linalg.matfuncs', None),
+                          ('scipy.linalg.misc', None),
+                          ('scipy.linalg.special_matrices', None),
+                          ('scipy.misc.common', None),
+                          ('scipy.ndimage.filters', None),
+                          ('scipy.ndimage.fourier', None),
+                          ('scipy.ndimage.interpolation', None),
+                          ('scipy.ndimage.measurements', None),
+                          ('scipy.ndimage.morphology', None),
+                          ('scipy.odr.models', None),
+                          ('scipy.odr.odrpack', None),
+                          ('scipy.optimize.cobyla', None),
+                          ('scipy.optimize.lbfgsb', None),
+                          ('scipy.optimize.linesearch', None),
+                          ('scipy.optimize.minpack', None),
+                          ('scipy.optimize.minpack2', None),
+                          ('scipy.optimize.moduleTNC', None),
+                          ('scipy.optimize.nonlin', None),
+                          ('scipy.optimize.optimize', None),
+                          ('scipy.optimize.slsqp', None),
+                          ('scipy.optimize.tnc', None),
+                          ('scipy.optimize.zeros', None),
+                          ('scipy.signal.bsplines', None),
+                          ('scipy.signal.filter_design', None),
+                          ('scipy.signal.fir_filter_design', None),
+                          ('scipy.signal.lti_conversion', None),
+                          ('scipy.signal.ltisys', None),
+                          ('scipy.signal.signaltools', None),
+                          ('scipy.signal.spectral', None),
+                          ('scipy.signal.waveforms', None),
+                          ('scipy.signal.wavelets', None),
+                          ('scipy.signal.windows.windows', 'windows'),
+                          ('scipy.sparse.lil', None),
+                          ('scipy.sparse.linalg.dsolve', 'linalg'),
+                          ('scipy.sparse.linalg.eigen', 'linalg'),
+                          ('scipy.sparse.linalg.interface', 'linalg'),
+                          ('scipy.sparse.linalg.isolve', 'linalg'),
+                          ('scipy.sparse.linalg.matfuncs', 'linalg'),
+                          ('scipy.sparse.sparsetools', None),
+                          ('scipy.sparse.spfuncs', None),
+                          ('scipy.sparse.sputils', None),
+                          ('scipy.spatial.ckdtree', None),
+                          ('scipy.spatial.kdtree', None),
+                          ('scipy.spatial.qhull', None),
+                          ('scipy.spatial.transform.rotation', 'transform'),
+                          ('scipy.special.add_newdocs', None),
+                          ('scipy.special.basic', None),
+                          ('scipy.special.orthogonal', None),
+                          ('scipy.special.sf_error', None),
+                          ('scipy.special.specfun', None),
+                          ('scipy.special.spfun_stats', None),
+                          ('scipy.stats.biasedurn', None),
+                          ('scipy.stats.kde', None),
+                          ('scipy.stats.morestats', None),
+                          ('scipy.stats.mstats_basic', 'mstats'),
+                          ('scipy.stats.mstats_extras', 'mstats'),
+                          ('scipy.stats.mvn', None),
+                          ('scipy.stats.stats', None)])
+def test_private_but_present_deprecation(module_name, correct_module):
+    # gh-18279, gh-17572, gh-17771 noted that deprecation warnings
+    # for imports from private modules
+    # were misleading. Check that this is resolved.
+    module = import_module(module_name)
+    if correct_module is None:
+        import_name = f'scipy.{module_name.split(".")[1]}'
+    else:
+        import_name = f'scipy.{module_name.split(".")[1]}.{correct_module}'
+
+    correct_import = import_module(import_name)
+
+    # Attributes that were formerly in `module_name` can still be imported from
+    # `module_name`, albeit with a deprecation warning.
+    for attr_name in module.__all__:
+        if attr_name == "varmats_from_mat":
+            # defer handling this case, see
+            # https://github.com/scipy/scipy/issues/19223
+            continue
+        # ensure attribute is present where the warning is pointing
+        assert getattr(correct_import, attr_name, None) is not None
+        message = f"Please import `{attr_name}` from the `{import_name}`..."
+        with pytest.deprecated_call(match=message):
+            getattr(module, attr_name)
+
+    # Attributes that were not in `module_name` get an error notifying the user
+    # that the attribute is not in `module_name` and that `module_name` is deprecated.
+    message = f"`{module_name}` is deprecated..."
+    with pytest.raises(AttributeError, match=message):
+        getattr(module, "ekki")
+
+
+def test_misc_doccer_deprecation():
+    # gh-18279, gh-17572, gh-17771 noted that deprecation warnings
+    # for imports from private modules were misleading.
+    # Check that this is resolved.
+    # `test_private_but_present_deprecation` cannot be used since `correct_import`
+    # is a different subpackage (`_lib` instead of `misc`).
+    module = import_module('scipy.misc.doccer')
+    correct_import = import_module('scipy._lib.doccer')
+
+    # Attributes that were formerly in `scipy.misc.doccer` can still be imported from
+    # `scipy.misc.doccer`, albeit with a deprecation warning. The specific message
+    # depends on whether the attribute is in `scipy._lib.doccer` or not.
+    for attr_name in module.__all__:
+        attr = getattr(correct_import, attr_name, None)
+        if attr is None:
+            message = f"`scipy.misc.{attr_name}` is deprecated..."
+        else:
+            message = f"Please import `{attr_name}` from the `scipy._lib.doccer`..."
+        with pytest.deprecated_call(match=message):
+            getattr(module, attr_name)
+
+    # Attributes that were not in `scipy.misc.doccer` get an error
+    # notifying the user that the attribute is not in `scipy.misc.doccer` 
+    # and that `scipy.misc.doccer` is deprecated.
+    message = "`scipy.misc.doccer` is deprecated..."
+    with pytest.raises(AttributeError, match=message):
+        getattr(module, "ekki")
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_scipy_version.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_scipy_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..21f0e8e26aad7500fee2fec9e845d5cf6caea4c6
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_scipy_version.py
@@ -0,0 +1,18 @@
+import re
+
+import scipy
+from numpy.testing import assert_
+
+
+def test_valid_scipy_version():
+    # Verify that the SciPy version is a valid one (no .post suffix or other
+    # nonsense). See NumPy issue gh-6431 for an issue caused by an invalid
+    # version.
+    version_pattern = r"^[0-9]+\.[0-9]+\.[0-9]+(|a[0-9]|b[0-9]|rc[0-9])"
+    dev_suffix = r"(\.dev0\+.+([0-9a-f]{7}|Unknown))"
+    if scipy.version.release:
+        res = re.match(version_pattern, scipy.__version__)
+    else:
+        res = re.match(version_pattern + dev_suffix, scipy.__version__)
+
+    assert_(res is not None, scipy.__version__)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_tmpdirs.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_tmpdirs.py
new file mode 100644
index 0000000000000000000000000000000000000000..734f42b32f8124924a7243b188f903eca40401e9
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_tmpdirs.py
@@ -0,0 +1,42 @@
+""" Test tmpdirs module """
+from os import getcwd
+from os.path import realpath, abspath, dirname, isfile, join as pjoin, exists
+
+from scipy._lib._tmpdirs import tempdir, in_tempdir, in_dir
+
+from numpy.testing import assert_, assert_equal
+
+MY_PATH = abspath(__file__)
+MY_DIR = dirname(MY_PATH)
+
+
+def test_tempdir():
+    with tempdir() as tmpdir:
+        fname = pjoin(tmpdir, 'example_file.txt')
+        with open(fname, "w") as fobj:
+            fobj.write('a string\\n')
+    assert_(not exists(tmpdir))
+
+
+def test_in_tempdir():
+    my_cwd = getcwd()
+    with in_tempdir() as tmpdir:
+        with open('test.txt', "w") as f:
+            f.write('some text')
+        assert_(isfile('test.txt'))
+        assert_(isfile(pjoin(tmpdir, 'test.txt')))
+    assert_(not exists(tmpdir))
+    assert_equal(getcwd(), my_cwd)
+
+
+def test_given_directory():
+    # Test InGivenDirectory
+    cwd = getcwd()
+    with in_dir() as tmpdir:
+        assert_equal(tmpdir, abspath(cwd))
+        assert_equal(tmpdir, abspath(getcwd()))
+    with in_dir(MY_DIR) as tmpdir:
+        assert_equal(tmpdir, MY_DIR)
+        assert_equal(realpath(MY_DIR), realpath(abspath(getcwd())))
+    # We were deleting the given directory! Check not so now.
+    assert_(isfile(MY_PATH))
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_warnings.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_warnings.py
new file mode 100644
index 0000000000000000000000000000000000000000..570ea017a8f1b4db9eb67ef9e931ef15f0c39bd1
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/tests/test_warnings.py
@@ -0,0 +1,135 @@
+"""
+Tests which scan for certain occurrences in the code, they may not find
+all of these occurrences but should catch almost all. This file was adapted
+from NumPy.
+"""
+
+
+import os
+from pathlib import Path
+import ast
+import tokenize
+
+import scipy
+
+import pytest
+
+
+class ParseCall(ast.NodeVisitor):
+    def __init__(self):
+        self.ls = []
+
+    def visit_Attribute(self, node):
+        ast.NodeVisitor.generic_visit(self, node)
+        self.ls.append(node.attr)
+
+    def visit_Name(self, node):
+        self.ls.append(node.id)
+
+
+class FindFuncs(ast.NodeVisitor):
+    def __init__(self, filename):
+        super().__init__()
+        self.__filename = filename
+        self.bad_filters = []
+        self.bad_stacklevels = []
+
+    def visit_Call(self, node):
+        p = ParseCall()
+        p.visit(node.func)
+        ast.NodeVisitor.generic_visit(self, node)
+
+        if p.ls[-1] == 'simplefilter' or p.ls[-1] == 'filterwarnings':
+            # get first argument of the `args` node of the filter call
+            match node.args[0]:
+                case ast.Constant() as c:
+                    argtext = c.value
+                case ast.JoinedStr() as js:
+                    # if we get an f-string, discard the templated pieces, which
+                    # are likely the type or specific message; we're interested
+                    # in the action, which is less likely to use a template
+                    argtext = "".join(
+                        x.value for x in js.values if isinstance(x, ast.Constant)
+                    )
+                case _:
+                    raise ValueError("unknown ast node type")
+            # check if filter is set to ignore
+            if argtext == "ignore":
+                self.bad_filters.append(
+                    f"{self.__filename}:{node.lineno}")
+
+        if p.ls[-1] == 'warn' and (
+                len(p.ls) == 1 or p.ls[-2] == 'warnings'):
+
+            if self.__filename == "_lib/tests/test_warnings.py":
+                # This file
+                return
+
+            # See if stacklevel exists:
+            if len(node.args) == 3:
+                return
+            args = {kw.arg for kw in node.keywords}
+            if "stacklevel" not in args:
+                self.bad_stacklevels.append(
+                    f"{self.__filename}:{node.lineno}")
+
+
+@pytest.fixture(scope="session")
+def warning_calls():
+    # combined "ignore" and stacklevel error
+    base = Path(scipy.__file__).parent
+
+    bad_filters = []
+    bad_stacklevels = []
+
+    for path in base.rglob("*.py"):
+        # use tokenize to auto-detect encoding on systems where no
+        # default encoding is defined (e.g., LANG='C')
+        with tokenize.open(str(path)) as file:
+            tree = ast.parse(file.read(), filename=str(path))
+            finder = FindFuncs(path.relative_to(base))
+            finder.visit(tree)
+            bad_filters.extend(finder.bad_filters)
+            bad_stacklevels.extend(finder.bad_stacklevels)
+
+    return bad_filters, bad_stacklevels
+
+
+@pytest.mark.fail_slow(20)
+@pytest.mark.slow
+def test_warning_calls_filters(warning_calls):
+    bad_filters, bad_stacklevels = warning_calls
+
+    # We try not to add filters in the code base, because those filters aren't
+    # thread-safe. We aim to only filter in tests with
+    # np.testing.suppress_warnings. However, in some cases it may prove
+    # necessary to filter out warnings, because we can't (easily) fix the root
+    # cause for them and we don't want users to see some warnings when they use
+    # SciPy correctly. So we list exceptions here.  Add new entries only if
+    # there's a good reason.
+    allowed_filters = (
+        os.path.join('datasets', '_fetchers.py'),
+        os.path.join('datasets', '__init__.py'),
+        os.path.join('optimize', '_optimize.py'),
+        os.path.join('optimize', '_constraints.py'),
+        os.path.join('optimize', '_nnls.py'),
+        os.path.join('signal', '_ltisys.py'),
+        os.path.join('sparse', '__init__.py'),  # np.matrix pending-deprecation
+        os.path.join('stats', '_discrete_distns.py'),  # gh-14901
+        os.path.join('stats', '_continuous_distns.py'),
+        os.path.join('stats', '_binned_statistic.py'),  # gh-19345
+        os.path.join('stats', 'tests', 'test_axis_nan_policy.py'),  # gh-20694
+        os.path.join('_lib', '_util.py'),  # gh-19341
+        os.path.join('sparse', 'linalg', '_dsolve', 'linsolve.py'), # gh-17924
+        "conftest.py",
+    )
+    bad_filters = [item for item in bad_filters if item.split(':')[0] not in
+                   allowed_filters]
+
+    if bad_filters:
+        raise AssertionError(
+            "warning ignore filter should not be used, instead, use\n"
+            "numpy.testing.suppress_warnings (in tests only);\n"
+            "found in:\n    {}".format(
+                "\n    ".join(bad_filters)))
+
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/uarray.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/uarray.py
new file mode 100644
index 0000000000000000000000000000000000000000..b29fc713efb3e836cc179ac87ce41f87b51870ef
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/_lib/uarray.py
@@ -0,0 +1,31 @@
+"""`uarray` provides functions for generating multimethods that dispatch to
+multiple different backends
+
+This should be imported, rather than `_uarray` so that an installed version could
+be used instead, if available. This means that users can call
+`uarray.set_backend` directly instead of going through SciPy.
+
+"""
+
+
+# Prefer an installed version of uarray, if available
+try:
+    import uarray as _uarray
+except ImportError:
+    _has_uarray = False
+else:
+    from scipy._lib._pep440 import Version as _Version
+
+    _has_uarray = _Version(_uarray.__version__) >= _Version("0.8")
+    del _uarray
+    del _Version
+
+
+if _has_uarray:
+    from uarray import *  # noqa: F403
+    from uarray import _Function
+else:
+    from ._uarray import *  # noqa: F403
+    from ._uarray import _Function  # noqa: F401
+
+del _has_uarray
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__init__.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..08e2e13478365fb7b227b461418d2f31e3cb76d2
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__init__.py
@@ -0,0 +1,90 @@
+"""
+================================
+Datasets (:mod:`scipy.datasets`)
+================================
+
+.. currentmodule:: scipy.datasets
+
+Dataset Methods
+===============
+
+.. autosummary::
+   :toctree: generated/
+
+   ascent
+   face
+   electrocardiogram
+
+Utility Methods
+===============
+
+.. autosummary::
+   :toctree: generated/
+
+   download_all    -- Download all the dataset files to specified path.
+   clear_cache     -- Clear cached dataset directory.
+
+
+Usage of Datasets
+=================
+
+SciPy dataset methods can be simply called as follows: ``'()'``
+This downloads the dataset files over the network once, and saves the cache,
+before returning a `numpy.ndarray` object representing the dataset.
+
+Note that the return data structure and data type might be different for
+different dataset methods. For a more detailed example on usage, please look
+into the particular dataset method documentation above.
+
+
+How dataset retrieval and storage works
+=======================================
+
+SciPy dataset files are stored within individual github repositories under the
+SciPy GitHub organization, following a naming convention as
+``'dataset-'``, for example `scipy.datasets.face` files live at
+https://github.com/scipy/dataset-face.  The `scipy.datasets` submodule utilizes
+and depends on `Pooch `_, a Python
+package built to simplify fetching data files. Pooch uses these repos to
+retrieve the respective dataset files when calling the dataset function.
+
+A registry of all the datasets, essentially a mapping of filenames with their
+SHA256 hash and repo urls are maintained, which Pooch uses to handle and verify
+the downloads on function call. After downloading the dataset once, the files
+are saved in the system cache directory under ``'scipy-data'``.
+
+Dataset cache locations may vary on different platforms.
+
+For macOS::
+
+    '~/Library/Caches/scipy-data'
+
+For Linux and other Unix-like platforms::
+
+    '~/.cache/scipy-data'  # or the value of the XDG_CACHE_HOME env var, if defined
+
+For Windows::
+
+    'C:\\Users\\\\AppData\\Local\\\\scipy-data\\Cache'
+
+
+In environments with constrained network connectivity for various security
+reasons or on systems without continuous internet connections, one may manually
+load the cache of the datasets by placing the contents of the dataset repo in
+the above mentioned cache directory to avoid fetching dataset errors without
+the internet connectivity.
+
+"""
+
+
+from ._fetchers import face, ascent, electrocardiogram
+from ._download_all import download_all
+from ._utils import clear_cache
+
+__all__ = ['ascent', 'electrocardiogram', 'face',
+           'download_all', 'clear_cache']
+
+
+from scipy._lib._testutils import PytestTester
+test = PytestTester(__name__)
+del PytestTester
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/__init__.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a47c097a3c9d33754bb26b9214b348ad79fe2c16
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/__init__.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/_download_all.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/_download_all.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ceb5795571161b1e4aa7407575642dfb9c91eea7
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/_download_all.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/_fetchers.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/_fetchers.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..179cbd0dd7d9b32d26e664eff79c587fe15723d2
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/_fetchers.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/_registry.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/_registry.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b993f03d1d8ea291a576a11fab3b68ce7bc6334b
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/_registry.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/_utils.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/_utils.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..358d8455ea6f0526244bbc7ccbc572005ffa1aaf
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/__pycache__/_utils.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/_download_all.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/_download_all.py
new file mode 100644
index 0000000000000000000000000000000000000000..255fdcaf22950848f458a7ed9ada183e0a2e630e
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/_download_all.py
@@ -0,0 +1,57 @@
+"""
+Platform independent script to download all the
+`scipy.datasets` module data files.
+This doesn't require a full scipy build.
+
+Run: python _download_all.py 
+"""
+
+import argparse
+try:
+    import pooch
+except ImportError:
+    pooch = None
+
+
+if __package__ is None or __package__ == '':
+    # Running as python script, use absolute import
+    import _registry  # type: ignore
+else:
+    # Running as python module, use relative import
+    from . import _registry
+
+
+def download_all(path=None):
+    """
+    Utility method to download all the dataset files
+    for `scipy.datasets` module.
+
+    Parameters
+    ----------
+    path : str, optional
+        Directory path to download all the dataset files.
+        If None, default to the system cache_dir detected by pooch.
+    """
+    if pooch is None:
+        raise ImportError("Missing optional dependency 'pooch' required "
+                          "for scipy.datasets module. Please use pip or "
+                          "conda to install 'pooch'.")
+    if path is None:
+        path = pooch.os_cache('scipy-data')
+    for dataset_name, dataset_hash in _registry.registry.items():
+        pooch.retrieve(url=_registry.registry_urls[dataset_name],
+                       known_hash=dataset_hash,
+                       fname=dataset_name, path=path)
+
+
+def main():
+    parser = argparse.ArgumentParser(description='Download SciPy data files.')
+    parser.add_argument("path", nargs='?', type=str,
+                        default=pooch.os_cache('scipy-data'),
+                        help="Directory path to download all the data files.")
+    args = parser.parse_args()
+    download_all(args.path)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/_fetchers.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/_fetchers.py
new file mode 100644
index 0000000000000000000000000000000000000000..f273b9eb826dde77d197e16ea92511bbd237b3d4
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/_fetchers.py
@@ -0,0 +1,221 @@
+from numpy import array, frombuffer, load
+from ._registry import registry, registry_urls
+
+try:
+    import pooch
+except ImportError:
+    pooch = None
+    data_fetcher = None
+else:
+    data_fetcher = pooch.create(
+        # Use the default cache folder for the operating system
+        # Pooch uses appdirs (https://github.com/ActiveState/appdirs) to
+        # select an appropriate directory for the cache on each platform.
+        path=pooch.os_cache("scipy-data"),
+
+        # The remote data is on Github
+        # base_url is a required param, even though we override this
+        # using individual urls in the registry.
+        base_url="https://github.com/scipy/",
+        registry=registry,
+        urls=registry_urls
+    )
+
+
+def fetch_data(dataset_name, data_fetcher=data_fetcher):
+    if data_fetcher is None:
+        raise ImportError("Missing optional dependency 'pooch' required "
+                          "for scipy.datasets module. Please use pip or "
+                          "conda to install 'pooch'.")
+    # The "fetch" method returns the full path to the downloaded data file.
+    return data_fetcher.fetch(dataset_name)
+
+
+def ascent():
+    """
+    Get an 8-bit grayscale bit-depth, 512 x 512 derived image for easy
+    use in demos.
+
+    The image is derived from
+    https://pixnio.com/people/accent-to-the-top
+
+    Parameters
+    ----------
+    None
+
+    Returns
+    -------
+    ascent : ndarray
+       convenient image to use for testing and demonstration
+
+    Examples
+    --------
+    >>> import scipy.datasets
+    >>> ascent = scipy.datasets.ascent()
+    >>> ascent.shape
+    (512, 512)
+    >>> ascent.max()
+    255
+
+    >>> import matplotlib.pyplot as plt
+    >>> plt.gray()
+    >>> plt.imshow(ascent)
+    >>> plt.show()
+
+    """
+    import pickle
+
+    # The file will be downloaded automatically the first time this is run,
+    # returning the path to the downloaded file. Afterwards, Pooch finds
+    # it in the local cache and doesn't repeat the download.
+    fname = fetch_data("ascent.dat")
+    # Now we just need to load it with our standard Python tools.
+    with open(fname, 'rb') as f:
+        ascent = array(pickle.load(f))
+    return ascent
+
+
+def electrocardiogram():
+    """
+    Load an electrocardiogram as an example for a 1-D signal.
+
+    The returned signal is a 5 minute long electrocardiogram (ECG), a medical
+    recording of the heart's electrical activity, sampled at 360 Hz.
+
+    Returns
+    -------
+    ecg : ndarray
+        The electrocardiogram in millivolt (mV) sampled at 360 Hz.
+
+    Notes
+    -----
+    The provided signal is an excerpt (19:35 to 24:35) from the `record 208`_
+    (lead MLII) provided by the MIT-BIH Arrhythmia Database [1]_ on
+    PhysioNet [2]_. The excerpt includes noise induced artifacts, typical
+    heartbeats as well as pathological changes.
+
+    .. _record 208: https://physionet.org/physiobank/database/html/mitdbdir/records.htm#208
+
+    .. versionadded:: 1.1.0
+
+    References
+    ----------
+    .. [1] Moody GB, Mark RG. The impact of the MIT-BIH Arrhythmia Database.
+           IEEE Eng in Med and Biol 20(3):45-50 (May-June 2001).
+           (PMID: 11446209); :doi:`10.13026/C2F305`
+    .. [2] Goldberger AL, Amaral LAN, Glass L, Hausdorff JM, Ivanov PCh,
+           Mark RG, Mietus JE, Moody GB, Peng C-K, Stanley HE. PhysioBank,
+           PhysioToolkit, and PhysioNet: Components of a New Research Resource
+           for Complex Physiologic Signals. Circulation 101(23):e215-e220;
+           :doi:`10.1161/01.CIR.101.23.e215`
+
+    Examples
+    --------
+    >>> from scipy.datasets import electrocardiogram
+    >>> ecg = electrocardiogram()
+    >>> ecg
+    array([-0.245, -0.215, -0.185, ..., -0.405, -0.395, -0.385])
+    >>> ecg.shape, ecg.mean(), ecg.std()
+    ((108000,), -0.16510875, 0.5992473991177294)
+
+    As stated the signal features several areas with a different morphology.
+    E.g., the first few seconds show the electrical activity of a heart in
+    normal sinus rhythm as seen below.
+
+    >>> import numpy as np
+    >>> import matplotlib.pyplot as plt
+    >>> fs = 360
+    >>> time = np.arange(ecg.size) / fs
+    >>> plt.plot(time, ecg)
+    >>> plt.xlabel("time in s")
+    >>> plt.ylabel("ECG in mV")
+    >>> plt.xlim(9, 10.2)
+    >>> plt.ylim(-1, 1.5)
+    >>> plt.show()
+
+    After second 16, however, the first premature ventricular contractions,
+    also called extrasystoles, appear. These have a different morphology
+    compared to typical heartbeats. The difference can easily be observed
+    in the following plot.
+
+    >>> plt.plot(time, ecg)
+    >>> plt.xlabel("time in s")
+    >>> plt.ylabel("ECG in mV")
+    >>> plt.xlim(46.5, 50)
+    >>> plt.ylim(-2, 1.5)
+    >>> plt.show()
+
+    At several points large artifacts disturb the recording, e.g.:
+
+    >>> plt.plot(time, ecg)
+    >>> plt.xlabel("time in s")
+    >>> plt.ylabel("ECG in mV")
+    >>> plt.xlim(207, 215)
+    >>> plt.ylim(-2, 3.5)
+    >>> plt.show()
+
+    Finally, examining the power spectrum reveals that most of the biosignal is
+    made up of lower frequencies. At 60 Hz the noise induced by the mains
+    electricity can be clearly observed.
+
+    >>> from scipy.signal import welch
+    >>> f, Pxx = welch(ecg, fs=fs, nperseg=2048, scaling="spectrum")
+    >>> plt.semilogy(f, Pxx)
+    >>> plt.xlabel("Frequency in Hz")
+    >>> plt.ylabel("Power spectrum of the ECG in mV**2")
+    >>> plt.xlim(f[[0, -1]])
+    >>> plt.show()
+    """
+    fname = fetch_data("ecg.dat")
+    with load(fname) as file:
+        ecg = file["ecg"].astype(int)  # np.uint16 -> int
+    # Convert raw output of ADC to mV: (ecg - adc_zero) / adc_gain
+    ecg = (ecg - 1024) / 200.0
+    return ecg
+
+
+def face(gray=False):
+    """
+    Get a 1024 x 768, color image of a raccoon face.
+
+    The image is derived from
+    https://pixnio.com/fauna-animals/raccoons/raccoon-procyon-lotor
+
+    Parameters
+    ----------
+    gray : bool, optional
+        If True return 8-bit grey-scale image, otherwise return a color image
+
+    Returns
+    -------
+    face : ndarray
+        image of a raccoon face
+
+    Examples
+    --------
+    >>> import scipy.datasets
+    >>> face = scipy.datasets.face()
+    >>> face.shape
+    (768, 1024, 3)
+    >>> face.max()
+    255
+    >>> face.dtype
+    dtype('uint8')
+
+    >>> import matplotlib.pyplot as plt
+    >>> plt.gray()
+    >>> plt.imshow(face)
+    >>> plt.show()
+
+    """
+    import bz2
+    fname = fetch_data("face.dat")
+    with open(fname, 'rb') as f:
+        rawdata = f.read()
+    face_data = bz2.decompress(rawdata)
+    face = frombuffer(face_data, dtype='uint8')
+    face.shape = (768, 1024, 3)
+    if gray is True:
+        face = (0.21 * face[:, :, 0] + 0.71 * face[:, :, 1] +
+                0.07 * face[:, :, 2]).astype('uint8')
+    return face
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/_registry.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/_registry.py
new file mode 100644
index 0000000000000000000000000000000000000000..969384ad9843159e766100bfa9755aed8102dd09
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/_registry.py
@@ -0,0 +1,26 @@
+##########################################################################
+# This file serves as the dataset registry for SciPy Datasets SubModule.
+##########################################################################
+
+
+# To generate the SHA256 hash, use the command
+# openssl sha256 
+registry = {
+    "ascent.dat": "03ce124c1afc880f87b55f6b061110e2e1e939679184f5614e38dacc6c1957e2",
+    "ecg.dat": "f20ad3365fb9b7f845d0e5c48b6fe67081377ee466c3a220b7f69f35c8958baf",
+    "face.dat": "9d8b0b4d081313e2b485748c770472e5a95ed1738146883d84c7030493e82886"
+}
+
+registry_urls = {
+    "ascent.dat": "https://raw.githubusercontent.com/scipy/dataset-ascent/main/ascent.dat",
+    "ecg.dat": "https://raw.githubusercontent.com/scipy/dataset-ecg/main/ecg.dat",
+    "face.dat": "https://raw.githubusercontent.com/scipy/dataset-face/main/face.dat"
+}
+
+# dataset method mapping with their associated filenames
+#  : ["filename1", "filename2", ...]
+method_files_map = {
+    "ascent": ["ascent.dat"],
+    "electrocardiogram": ["ecg.dat"],
+    "face": ["face.dat"]
+}
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/_utils.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f644f8797d6e3256a16ec2c509eec725c726300
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/_utils.py
@@ -0,0 +1,81 @@
+import os
+import shutil
+from ._registry import method_files_map
+
+try:
+    import platformdirs
+except ImportError:
+    platformdirs = None  # type: ignore[assignment]
+
+
+def _clear_cache(datasets, cache_dir=None, method_map=None):
+    if method_map is None:
+        # Use SciPy Datasets method map
+        method_map = method_files_map
+    if cache_dir is None:
+        # Use default cache_dir path
+        if platformdirs is None:
+            # platformdirs is pooch dependency
+            raise ImportError("Missing optional dependency 'pooch' required "
+                              "for scipy.datasets module. Please use pip or "
+                              "conda to install 'pooch'.")
+        cache_dir = platformdirs.user_cache_dir("scipy-data")
+
+    if not os.path.exists(cache_dir):
+        print(f"Cache Directory {cache_dir} doesn't exist. Nothing to clear.")
+        return
+
+    if datasets is None:
+        print(f"Cleaning the cache directory {cache_dir}!")
+        shutil.rmtree(cache_dir)
+    else:
+        if not isinstance(datasets, (list, tuple)):
+            # single dataset method passed should be converted to list
+            datasets = [datasets, ]
+        for dataset in datasets:
+            assert callable(dataset)
+            dataset_name = dataset.__name__  # Name of the dataset method
+            if dataset_name not in method_map:
+                raise ValueError(f"Dataset method {dataset_name} doesn't "
+                                 "exist. Please check if the passed dataset "
+                                 "is a subset of the following dataset "
+                                 f"methods: {list(method_map.keys())}")
+
+            data_files = method_map[dataset_name]
+            data_filepaths = [os.path.join(cache_dir, file)
+                              for file in data_files]
+            for data_filepath in data_filepaths:
+                if os.path.exists(data_filepath):
+                    print("Cleaning the file "
+                          f"{os.path.split(data_filepath)[1]} "
+                          f"for dataset {dataset_name}")
+                    os.remove(data_filepath)
+                else:
+                    print(f"Path {data_filepath} doesn't exist. "
+                          "Nothing to clear.")
+
+
+def clear_cache(datasets=None):
+    """
+    Cleans the scipy datasets cache directory.
+
+    If a scipy.datasets method or a list/tuple of the same is
+    provided, then clear_cache removes all the data files
+    associated to the passed dataset method callable(s).
+
+    By default, it removes all the cached data files.
+
+    Parameters
+    ----------
+    datasets : callable or list/tuple of callable or None
+
+    Examples
+    --------
+    >>> from scipy import datasets
+    >>> ascent_array = datasets.ascent()
+    >>> ascent_array.shape
+    (512, 512)
+    >>> datasets.clear_cache([datasets.ascent])
+    Cleaning the file ascent.dat for dataset ascent
+    """
+    _clear_cache(datasets)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/tests/__init__.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/tests/__pycache__/__init__.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/tests/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..cbf30e6d4fc9c0a389a83748c3acf41bc7c13d0c
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/tests/__pycache__/__init__.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/tests/__pycache__/test_data.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/tests/__pycache__/test_data.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1d3b0a75b63e9ded49993872802416be7eabadbb
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/tests/__pycache__/test_data.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/tests/test_data.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/tests/test_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..d29feb72f55f564d3bda3c5cfa956bf050e14135
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/datasets/tests/test_data.py
@@ -0,0 +1,124 @@
+from scipy.datasets._registry import registry
+from scipy.datasets._fetchers import data_fetcher
+from scipy.datasets._utils import _clear_cache
+from scipy.datasets import ascent, face, electrocardiogram, download_all
+from numpy.testing import assert_equal, assert_almost_equal
+import os
+import pytest
+
+try:
+    import pooch
+except ImportError:
+    raise ImportError("Missing optional dependency 'pooch' required "
+                      "for scipy.datasets module. Please use pip or "
+                      "conda to install 'pooch'.")
+
+
+data_dir = data_fetcher.path  # type: ignore
+
+
+def _has_hash(path, expected_hash):
+    """Check if the provided path has the expected hash."""
+    if not os.path.exists(path):
+        return False
+    return pooch.file_hash(path) == expected_hash
+
+
+class TestDatasets:
+
+    @pytest.fixture(scope='module', autouse=True)
+    def test_download_all(self):
+        # This fixture requires INTERNET CONNECTION
+
+        # test_setup phase
+        download_all()
+
+        yield
+
+    @pytest.mark.fail_slow(5)
+    def test_existence_all(self):
+        assert len(os.listdir(data_dir)) >= len(registry)
+
+    def test_ascent(self):
+        assert_equal(ascent().shape, (512, 512))
+
+        # hash check
+        assert _has_hash(os.path.join(data_dir, "ascent.dat"),
+                         registry["ascent.dat"])
+
+    def test_face(self):
+        assert_equal(face().shape, (768, 1024, 3))
+
+        # hash check
+        assert _has_hash(os.path.join(data_dir, "face.dat"),
+                         registry["face.dat"])
+
+    def test_electrocardiogram(self):
+        # Test shape, dtype and stats of signal
+        ecg = electrocardiogram()
+        assert_equal(ecg.dtype, float)
+        assert_equal(ecg.shape, (108000,))
+        assert_almost_equal(ecg.mean(), -0.16510875)
+        assert_almost_equal(ecg.std(), 0.5992473991177294)
+
+        # hash check
+        assert _has_hash(os.path.join(data_dir, "ecg.dat"),
+                         registry["ecg.dat"])
+
+
+def test_clear_cache(tmp_path):
+    # Note: `tmp_path` is a pytest fixture, it handles cleanup
+    dummy_basepath = tmp_path / "dummy_cache_dir"
+    dummy_basepath.mkdir()
+
+    # Create three dummy dataset files for dummy dataset methods
+    dummy_method_map = {}
+    for i in range(4):
+        dummy_method_map[f"data{i}"] = [f"data{i}.dat"]
+        data_filepath = dummy_basepath / f"data{i}.dat"
+        data_filepath.write_text("")
+
+    # clear files associated to single dataset method data0
+    # also test callable argument instead of list of callables
+    def data0():
+        pass
+    _clear_cache(datasets=data0, cache_dir=dummy_basepath,
+                 method_map=dummy_method_map)
+    assert not os.path.exists(dummy_basepath/"data0.dat")
+
+    # clear files associated to multiple dataset methods "data3" and "data4"
+    def data1():
+        pass
+
+    def data2():
+        pass
+    _clear_cache(datasets=[data1, data2], cache_dir=dummy_basepath,
+                 method_map=dummy_method_map)
+    assert not os.path.exists(dummy_basepath/"data1.dat")
+    assert not os.path.exists(dummy_basepath/"data2.dat")
+
+    # clear multiple dataset files "data3_0.dat" and "data3_1.dat"
+    # associated with dataset method "data3"
+    def data4():
+        pass
+    # create files
+    (dummy_basepath / "data4_0.dat").write_text("")
+    (dummy_basepath / "data4_1.dat").write_text("")
+
+    dummy_method_map["data4"] = ["data4_0.dat", "data4_1.dat"]
+    _clear_cache(datasets=[data4], cache_dir=dummy_basepath,
+                 method_map=dummy_method_map)
+    assert not os.path.exists(dummy_basepath/"data4_0.dat")
+    assert not os.path.exists(dummy_basepath/"data4_1.dat")
+
+    # wrong dataset method should raise ValueError since it
+    # doesn't exist in the dummy_method_map
+    def data5():
+        pass
+    with pytest.raises(ValueError):
+        _clear_cache(datasets=[data5], cache_dir=dummy_basepath,
+                     method_map=dummy_method_map)
+
+    # remove all dataset cache
+    _clear_cache(datasets=None, cache_dir=dummy_basepath)
+    assert not os.path.exists(dummy_basepath)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__init__.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f619dded6615a284392c4273559f226a1c8c72c
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__init__.py
@@ -0,0 +1,169 @@
+"""
+=========================================================
+Multidimensional image processing (:mod:`scipy.ndimage`)
+=========================================================
+
+.. currentmodule:: scipy.ndimage
+
+This package contains various functions for multidimensional image
+processing.
+
+
+Filters
+=======
+
+.. autosummary::
+   :toctree: generated/
+
+   convolve - Multidimensional convolution
+   convolve1d - 1-D convolution along the given axis
+   correlate - Multidimensional correlation
+   correlate1d - 1-D correlation along the given axis
+   gaussian_filter
+   gaussian_filter1d
+   gaussian_gradient_magnitude
+   gaussian_laplace
+   generic_filter - Multidimensional filter using a given function
+   generic_filter1d - 1-D generic filter along the given axis
+   generic_gradient_magnitude
+   generic_laplace
+   laplace - N-D Laplace filter based on approximate second derivatives
+   maximum_filter
+   maximum_filter1d
+   median_filter - Calculates a multidimensional median filter
+   minimum_filter
+   minimum_filter1d
+   percentile_filter - Calculates a multidimensional percentile filter
+   prewitt
+   rank_filter - Calculates a multidimensional rank filter
+   sobel
+   uniform_filter - Multidimensional uniform filter
+   uniform_filter1d - 1-D uniform filter along the given axis
+
+Fourier filters
+===============
+
+.. autosummary::
+   :toctree: generated/
+
+   fourier_ellipsoid
+   fourier_gaussian
+   fourier_shift
+   fourier_uniform
+
+Interpolation
+=============
+
+.. autosummary::
+   :toctree: generated/
+
+   affine_transform - Apply an affine transformation
+   geometric_transform - Apply an arbitrary geometric transform
+   map_coordinates - Map input array to new coordinates by interpolation
+   rotate - Rotate an array
+   shift - Shift an array
+   spline_filter
+   spline_filter1d
+   zoom - Zoom an array
+
+Measurements
+============
+
+.. autosummary::
+   :toctree: generated/
+
+   center_of_mass - The center of mass of the values of an array at labels
+   extrema - Min's and max's of an array at labels, with their positions
+   find_objects - Find objects in a labeled array
+   histogram - Histogram of the values of an array, optionally at labels
+   label - Label features in an array
+   labeled_comprehension
+   maximum
+   maximum_position
+   mean - Mean of the values of an array at labels
+   median
+   minimum
+   minimum_position
+   standard_deviation - Standard deviation of an N-D image array
+   sum_labels - Sum of the values of the array
+   value_indices - Find indices of each distinct value in given array
+   variance - Variance of the values of an N-D image array
+   watershed_ift
+
+Morphology
+==========
+
+.. autosummary::
+   :toctree: generated/
+
+   binary_closing
+   binary_dilation
+   binary_erosion
+   binary_fill_holes
+   binary_hit_or_miss
+   binary_opening
+   binary_propagation
+   black_tophat
+   distance_transform_bf
+   distance_transform_cdt
+   distance_transform_edt
+   generate_binary_structure
+   grey_closing
+   grey_dilation
+   grey_erosion
+   grey_opening
+   iterate_structure
+   morphological_gradient
+   morphological_laplace
+   white_tophat
+
+"""
+
+# Copyright (C) 2003-2005 Peter J. Verveer
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above
+#    copyright notice, this list of conditions and the following
+#    disclaimer in the documentation and/or other materials provided
+#    with the distribution.
+#
+# 3. The name of the author may not be used to endorse or promote
+#    products derived from this software without specific prior
+#    written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from ._filters import *
+from ._fourier import *
+from ._interpolation import *
+from ._measurements import *
+from ._morphology import *
+
+# Deprecated namespaces, to be removed in v2.0.0
+from . import filters
+from . import fourier
+from . import interpolation
+from . import measurements
+from . import morphology
+
+__all__ = [s for s in dir() if not s.startswith('_')]
+
+from scipy._lib._testutils import PytestTester
+test = PytestTester(__name__)
+del PytestTester
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/__init__.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..866f67038ced550b3bbe9d2a468327c66fadb753
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/__init__.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_filters.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_filters.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..17a6b7f0db9f773d6b966b0fa274811da0e1355e
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_filters.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_fourier.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_fourier.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f4c2e808e442ddf337e7bf86a3697eac3b2f319c
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_fourier.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_interpolation.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_interpolation.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ba8ca85312afbc371e5901a14eaa8349861a2c9a
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_interpolation.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_measurements.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_measurements.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f3b2ddfed0d89b5ec1014bc6d934688e57721b15
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_measurements.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_morphology.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_morphology.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e4aa9243f723128645d828ffb84f9c729ec773a8
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_morphology.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_ni_docstrings.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_ni_docstrings.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2928206b103d7d1426445c172a3a5b5599e88094
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_ni_docstrings.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_ni_support.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_ni_support.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..999efde4bf910b24cb428f4d3d3ef1d679640753
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/_ni_support.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/filters.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/filters.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..00aa022ec8c7d969148b88dd0a5f022d30f2ef85
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/filters.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/fourier.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/fourier.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..77403c6061156082b8522e0097572a2e9b1757c4
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/fourier.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/interpolation.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/interpolation.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d3cd209eba813ebda488dc689f41849b3f19a762
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/interpolation.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/measurements.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/measurements.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1f4fc88216c292b96202e149d6dd362ddefe0ac0
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/measurements.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/morphology.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/morphology.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5939f326757d4522937ee36f790d53d4ac4324c9
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/__pycache__/morphology.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_ctest.cpython-310-x86_64-linux-gnu.so b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_ctest.cpython-310-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..0d05e123ba1f7f45c1f37795e7ba5cd0257018b4
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_ctest.cpython-310-x86_64-linux-gnu.so differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_cytest.cpython-310-x86_64-linux-gnu.so b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_cytest.cpython-310-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..69dd431440c7266e26056b61d7bae98be2550957
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_cytest.cpython-310-x86_64-linux-gnu.so differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_filters.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..635b2d336b343ec832b0b411149063df505c5747
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_filters.py
@@ -0,0 +1,1858 @@
+# Copyright (C) 2003-2005 Peter J. Verveer
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above
+#    copyright notice, this list of conditions and the following
+#    disclaimer in the documentation and/or other materials provided
+#    with the distribution.
+#
+# 3. The name of the author may not be used to endorse or promote
+#    products derived from this software without specific prior
+#    written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from collections.abc import Iterable
+import numbers
+import warnings
+import numpy as np
+import operator
+
+from scipy._lib._util import normalize_axis_index
+from . import _ni_support
+from . import _nd_image
+from . import _ni_docstrings
+
+__all__ = ['correlate1d', 'convolve1d', 'gaussian_filter1d', 'gaussian_filter',
+           'prewitt', 'sobel', 'generic_laplace', 'laplace',
+           'gaussian_laplace', 'generic_gradient_magnitude',
+           'gaussian_gradient_magnitude', 'correlate', 'convolve',
+           'uniform_filter1d', 'uniform_filter', 'minimum_filter1d',
+           'maximum_filter1d', 'minimum_filter', 'maximum_filter',
+           'rank_filter', 'median_filter', 'percentile_filter',
+           'generic_filter1d', 'generic_filter']
+
+
+def _invalid_origin(origin, lenw):
+    return (origin < -(lenw // 2)) or (origin > (lenw - 1) // 2)
+
+
+def _complex_via_real_components(func, input, weights, output, cval, **kwargs):
+    """Complex convolution via a linear combination of real convolutions."""
+    complex_input = input.dtype.kind == 'c'
+    complex_weights = weights.dtype.kind == 'c'
+    if complex_input and complex_weights:
+        # real component of the output
+        func(input.real, weights.real, output=output.real,
+             cval=np.real(cval), **kwargs)
+        output.real -= func(input.imag, weights.imag, output=None,
+                            cval=np.imag(cval), **kwargs)
+        # imaginary component of the output
+        func(input.real, weights.imag, output=output.imag,
+             cval=np.real(cval), **kwargs)
+        output.imag += func(input.imag, weights.real, output=None,
+                            cval=np.imag(cval), **kwargs)
+    elif complex_input:
+        func(input.real, weights, output=output.real, cval=np.real(cval),
+             **kwargs)
+        func(input.imag, weights, output=output.imag, cval=np.imag(cval),
+             **kwargs)
+    else:
+        if np.iscomplexobj(cval):
+            raise ValueError("Cannot provide a complex-valued cval when the "
+                             "input is real.")
+        func(input, weights.real, output=output.real, cval=cval, **kwargs)
+        func(input, weights.imag, output=output.imag, cval=cval, **kwargs)
+    return output
+
+
+@_ni_docstrings.docfiller
+def correlate1d(input, weights, axis=-1, output=None, mode="reflect",
+                cval=0.0, origin=0):
+    """Calculate a 1-D correlation along the given axis.
+
+    The lines of the array along the given axis are correlated with the
+    given weights.
+
+    Parameters
+    ----------
+    %(input)s
+    weights : array
+        1-D sequence of numbers.
+    %(axis)s
+    %(output)s
+    %(mode_reflect)s
+    %(cval)s
+    %(origin)s
+
+    Returns
+    -------
+    result : ndarray
+        Correlation result. Has the same shape as `input`.
+
+    Examples
+    --------
+    >>> from scipy.ndimage import correlate1d
+    >>> correlate1d([2, 8, 0, 4, 1, 9, 9, 0], weights=[1, 3])
+    array([ 8, 26,  8, 12,  7, 28, 36,  9])
+    """
+    input = np.asarray(input)
+    weights = np.asarray(weights)
+    complex_input = input.dtype.kind == 'c'
+    complex_weights = weights.dtype.kind == 'c'
+    if complex_input or complex_weights:
+        if complex_weights:
+            weights = weights.conj()
+            weights = weights.astype(np.complex128, copy=False)
+        kwargs = dict(axis=axis, mode=mode, origin=origin)
+        output = _ni_support._get_output(output, input, complex_output=True)
+        return _complex_via_real_components(correlate1d, input, weights,
+                                            output, cval, **kwargs)
+
+    output = _ni_support._get_output(output, input)
+    weights = np.asarray(weights, dtype=np.float64)
+    if weights.ndim != 1 or weights.shape[0] < 1:
+        raise RuntimeError('no filter weights given')
+    if not weights.flags.contiguous:
+        weights = weights.copy()
+    axis = normalize_axis_index(axis, input.ndim)
+    if _invalid_origin(origin, len(weights)):
+        raise ValueError('Invalid origin; origin must satisfy '
+                         '-(len(weights) // 2) <= origin <= '
+                         '(len(weights)-1) // 2')
+    mode = _ni_support._extend_mode_to_code(mode)
+    _nd_image.correlate1d(input, weights, axis, output, mode, cval,
+                          origin)
+    return output
+
+
+@_ni_docstrings.docfiller
+def convolve1d(input, weights, axis=-1, output=None, mode="reflect",
+               cval=0.0, origin=0):
+    """Calculate a 1-D convolution along the given axis.
+
+    The lines of the array along the given axis are convolved with the
+    given weights.
+
+    Parameters
+    ----------
+    %(input)s
+    weights : ndarray
+        1-D sequence of numbers.
+    %(axis)s
+    %(output)s
+    %(mode_reflect)s
+    %(cval)s
+    %(origin)s
+
+    Returns
+    -------
+    convolve1d : ndarray
+        Convolved array with same shape as input
+
+    Examples
+    --------
+    >>> from scipy.ndimage import convolve1d
+    >>> convolve1d([2, 8, 0, 4, 1, 9, 9, 0], weights=[1, 3])
+    array([14, 24,  4, 13, 12, 36, 27,  0])
+    """
+    weights = weights[::-1]
+    origin = -origin
+    if not len(weights) & 1:
+        origin -= 1
+    weights = np.asarray(weights)
+    if weights.dtype.kind == 'c':
+        # pre-conjugate here to counteract the conjugation in correlate1d
+        weights = weights.conj()
+    return correlate1d(input, weights, axis, output, mode, cval, origin)
+
+
+def _gaussian_kernel1d(sigma, order, radius):
+    """
+    Computes a 1-D Gaussian convolution kernel.
+    """
+    if order < 0:
+        raise ValueError('order must be non-negative')
+    exponent_range = np.arange(order + 1)
+    sigma2 = sigma * sigma
+    x = np.arange(-radius, radius+1)
+    phi_x = np.exp(-0.5 / sigma2 * x ** 2)
+    phi_x = phi_x / phi_x.sum()
+
+    if order == 0:
+        return phi_x
+    else:
+        # f(x) = q(x) * phi(x) = q(x) * exp(p(x))
+        # f'(x) = (q'(x) + q(x) * p'(x)) * phi(x)
+        # p'(x) = -1 / sigma ** 2
+        # Implement q'(x) + q(x) * p'(x) as a matrix operator and apply to the
+        # coefficients of q(x)
+        q = np.zeros(order + 1)
+        q[0] = 1
+        D = np.diag(exponent_range[1:], 1)  # D @ q(x) = q'(x)
+        P = np.diag(np.ones(order)/-sigma2, -1)  # P @ q(x) = q(x) * p'(x)
+        Q_deriv = D + P
+        for _ in range(order):
+            q = Q_deriv.dot(q)
+        q = (x[:, None] ** exponent_range).dot(q)
+        return q * phi_x
+
+
+@_ni_docstrings.docfiller
+def gaussian_filter1d(input, sigma, axis=-1, order=0, output=None,
+                      mode="reflect", cval=0.0, truncate=4.0, *, radius=None):
+    """1-D Gaussian filter.
+
+    Parameters
+    ----------
+    %(input)s
+    sigma : scalar
+        standard deviation for Gaussian kernel
+    %(axis)s
+    order : int, optional
+        An order of 0 corresponds to convolution with a Gaussian
+        kernel. A positive order corresponds to convolution with
+        that derivative of a Gaussian.
+    %(output)s
+    %(mode_reflect)s
+    %(cval)s
+    truncate : float, optional
+        Truncate the filter at this many standard deviations.
+        Default is 4.0.
+    radius : None or int, optional
+        Radius of the Gaussian kernel. If specified, the size of
+        the kernel will be ``2*radius + 1``, and `truncate` is ignored.
+        Default is None.
+
+    Returns
+    -------
+    gaussian_filter1d : ndarray
+
+    Notes
+    -----
+    The Gaussian kernel will have size ``2*radius + 1`` along each axis. If
+    `radius` is None, a default ``radius = round(truncate * sigma)`` will be
+    used.
+
+    Examples
+    --------
+    >>> from scipy.ndimage import gaussian_filter1d
+    >>> import numpy as np
+    >>> gaussian_filter1d([1.0, 2.0, 3.0, 4.0, 5.0], 1)
+    array([ 1.42704095,  2.06782203,  3.        ,  3.93217797,  4.57295905])
+    >>> gaussian_filter1d([1.0, 2.0, 3.0, 4.0, 5.0], 4)
+    array([ 2.91948343,  2.95023502,  3.        ,  3.04976498,  3.08051657])
+    >>> import matplotlib.pyplot as plt
+    >>> rng = np.random.default_rng()
+    >>> x = rng.standard_normal(101).cumsum()
+    >>> y3 = gaussian_filter1d(x, 3)
+    >>> y6 = gaussian_filter1d(x, 6)
+    >>> plt.plot(x, 'k', label='original data')
+    >>> plt.plot(y3, '--', label='filtered, sigma=3')
+    >>> plt.plot(y6, ':', label='filtered, sigma=6')
+    >>> plt.legend()
+    >>> plt.grid()
+    >>> plt.show()
+
+    """
+    sd = float(sigma)
+    # make the radius of the filter equal to truncate standard deviations
+    lw = int(truncate * sd + 0.5)
+    if radius is not None:
+        lw = radius
+    if not isinstance(lw, numbers.Integral) or lw < 0:
+        raise ValueError('Radius must be a nonnegative integer.')
+    # Since we are calling correlate, not convolve, revert the kernel
+    weights = _gaussian_kernel1d(sigma, order, lw)[::-1]
+    return correlate1d(input, weights, axis, output, mode, cval, 0)
+
+
+@_ni_docstrings.docfiller
+def gaussian_filter(input, sigma, order=0, output=None,
+                    mode="reflect", cval=0.0, truncate=4.0, *, radius=None,
+                    axes=None):
+    """Multidimensional Gaussian filter.
+
+    Parameters
+    ----------
+    %(input)s
+    sigma : scalar or sequence of scalars
+        Standard deviation for Gaussian kernel. The standard
+        deviations of the Gaussian filter are given for each axis as a
+        sequence, or as a single number, in which case it is equal for
+        all axes.
+    order : int or sequence of ints, optional
+        The order of the filter along each axis is given as a sequence
+        of integers, or as a single number. An order of 0 corresponds
+        to convolution with a Gaussian kernel. A positive order
+        corresponds to convolution with that derivative of a Gaussian.
+    %(output)s
+    %(mode_multiple)s
+    %(cval)s
+    truncate : float, optional
+        Truncate the filter at this many standard deviations.
+        Default is 4.0.
+    radius : None or int or sequence of ints, optional
+        Radius of the Gaussian kernel. The radius are given for each axis
+        as a sequence, or as a single number, in which case it is equal
+        for all axes. If specified, the size of the kernel along each axis
+        will be ``2*radius + 1``, and `truncate` is ignored.
+        Default is None.
+    axes : tuple of int or None, optional
+        If None, `input` is filtered along all axes. Otherwise,
+        `input` is filtered along the specified axes. When `axes` is
+        specified, any tuples used for `sigma`, `order`, `mode` and/or `radius`
+        must match the length of `axes`. The ith entry in any of these tuples
+        corresponds to the ith entry in `axes`.
+
+    Returns
+    -------
+    gaussian_filter : ndarray
+        Returned array of same shape as `input`.
+
+    Notes
+    -----
+    The multidimensional filter is implemented as a sequence of
+    1-D convolution filters. The intermediate arrays are
+    stored in the same data type as the output. Therefore, for output
+    types with a limited precision, the results may be imprecise
+    because intermediate results may be stored with insufficient
+    precision.
+
+    The Gaussian kernel will have size ``2*radius + 1`` along each axis. If
+    `radius` is None, the default ``radius = round(truncate * sigma)`` will be
+    used.
+
+    Examples
+    --------
+    >>> from scipy.ndimage import gaussian_filter
+    >>> import numpy as np
+    >>> a = np.arange(50, step=2).reshape((5,5))
+    >>> a
+    array([[ 0,  2,  4,  6,  8],
+           [10, 12, 14, 16, 18],
+           [20, 22, 24, 26, 28],
+           [30, 32, 34, 36, 38],
+           [40, 42, 44, 46, 48]])
+    >>> gaussian_filter(a, sigma=1)
+    array([[ 4,  6,  8,  9, 11],
+           [10, 12, 14, 15, 17],
+           [20, 22, 24, 25, 27],
+           [29, 31, 33, 34, 36],
+           [35, 37, 39, 40, 42]])
+
+    >>> from scipy import datasets
+    >>> import matplotlib.pyplot as plt
+    >>> fig = plt.figure()
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ax1 = fig.add_subplot(121)  # left side
+    >>> ax2 = fig.add_subplot(122)  # right side
+    >>> ascent = datasets.ascent()
+    >>> result = gaussian_filter(ascent, sigma=5)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result)
+    >>> plt.show()
+    """
+    input = np.asarray(input)
+    output = _ni_support._get_output(output, input)
+
+    axes = _ni_support._check_axes(axes, input.ndim)
+    num_axes = len(axes)
+    orders = _ni_support._normalize_sequence(order, num_axes)
+    sigmas = _ni_support._normalize_sequence(sigma, num_axes)
+    modes = _ni_support._normalize_sequence(mode, num_axes)
+    radiuses = _ni_support._normalize_sequence(radius, num_axes)
+    axes = [(axes[ii], sigmas[ii], orders[ii], modes[ii], radiuses[ii])
+            for ii in range(num_axes) if sigmas[ii] > 1e-15]
+    if len(axes) > 0:
+        for axis, sigma, order, mode, radius in axes:
+            gaussian_filter1d(input, sigma, axis, order, output,
+                              mode, cval, truncate, radius=radius)
+            input = output
+    else:
+        output[...] = input[...]
+    return output
+
+
+@_ni_docstrings.docfiller
+def prewitt(input, axis=-1, output=None, mode="reflect", cval=0.0):
+    """Calculate a Prewitt filter.
+
+    Parameters
+    ----------
+    %(input)s
+    %(axis)s
+    %(output)s
+    %(mode_multiple)s
+    %(cval)s
+
+    Returns
+    -------
+    prewitt : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    See Also
+    --------
+    sobel: Sobel filter
+
+    Notes
+    -----
+    This function computes the one-dimensional Prewitt filter.
+    Horizontal edges are emphasised with the horizontal transform (axis=0),
+    vertical edges with the vertical transform (axis=1), and so on for higher
+    dimensions. These can be combined to give the magnitude.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> import numpy as np
+    >>> ascent = datasets.ascent()
+    >>> prewitt_h = ndimage.prewitt(ascent, axis=0)
+    >>> prewitt_v = ndimage.prewitt(ascent, axis=1)
+    >>> magnitude = np.sqrt(prewitt_h ** 2 + prewitt_v ** 2)
+    >>> magnitude *= 255 / np.max(magnitude) # Normalization
+    >>> fig, axes = plt.subplots(2, 2, figsize = (8, 8))
+    >>> plt.gray()
+    >>> axes[0, 0].imshow(ascent)
+    >>> axes[0, 1].imshow(prewitt_h)
+    >>> axes[1, 0].imshow(prewitt_v)
+    >>> axes[1, 1].imshow(magnitude)
+    >>> titles = ["original", "horizontal", "vertical", "magnitude"]
+    >>> for i, ax in enumerate(axes.ravel()):
+    ...     ax.set_title(titles[i])
+    ...     ax.axis("off")
+    >>> plt.show()
+
+    """
+    input = np.asarray(input)
+    axis = normalize_axis_index(axis, input.ndim)
+    output = _ni_support._get_output(output, input)
+    modes = _ni_support._normalize_sequence(mode, input.ndim)
+    correlate1d(input, [-1, 0, 1], axis, output, modes[axis], cval, 0)
+    axes = [ii for ii in range(input.ndim) if ii != axis]
+    for ii in axes:
+        correlate1d(output, [1, 1, 1], ii, output, modes[ii], cval, 0,)
+    return output
+
+
+@_ni_docstrings.docfiller
+def sobel(input, axis=-1, output=None, mode="reflect", cval=0.0):
+    """Calculate a Sobel filter.
+
+    Parameters
+    ----------
+    %(input)s
+    %(axis)s
+    %(output)s
+    %(mode_multiple)s
+    %(cval)s
+
+    Returns
+    -------
+    sobel : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    Notes
+    -----
+    This function computes the axis-specific Sobel gradient.
+    The horizontal edges can be emphasised with the horizontal transform (axis=0),
+    the vertical edges with the vertical transform (axis=1) and so on for higher
+    dimensions. These can be combined to give the magnitude.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> import numpy as np
+    >>> ascent = datasets.ascent().astype('int32')
+    >>> sobel_h = ndimage.sobel(ascent, 0)  # horizontal gradient
+    >>> sobel_v = ndimage.sobel(ascent, 1)  # vertical gradient
+    >>> magnitude = np.sqrt(sobel_h**2 + sobel_v**2)
+    >>> magnitude *= 255.0 / np.max(magnitude)  # normalization
+    >>> fig, axs = plt.subplots(2, 2, figsize=(8, 8))
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> axs[0, 0].imshow(ascent)
+    >>> axs[0, 1].imshow(sobel_h)
+    >>> axs[1, 0].imshow(sobel_v)
+    >>> axs[1, 1].imshow(magnitude)
+    >>> titles = ["original", "horizontal", "vertical", "magnitude"]
+    >>> for i, ax in enumerate(axs.ravel()):
+    ...     ax.set_title(titles[i])
+    ...     ax.axis("off")
+    >>> plt.show()
+
+    """
+    input = np.asarray(input)
+    axis = normalize_axis_index(axis, input.ndim)
+    output = _ni_support._get_output(output, input)
+    modes = _ni_support._normalize_sequence(mode, input.ndim)
+    correlate1d(input, [-1, 0, 1], axis, output, modes[axis], cval, 0)
+    axes = [ii for ii in range(input.ndim) if ii != axis]
+    for ii in axes:
+        correlate1d(output, [1, 2, 1], ii, output, modes[ii], cval, 0)
+    return output
+
+
+@_ni_docstrings.docfiller
+def generic_laplace(input, derivative2, output=None, mode="reflect",
+                    cval=0.0,
+                    extra_arguments=(),
+                    extra_keywords=None):
+    """
+    N-D Laplace filter using a provided second derivative function.
+
+    Parameters
+    ----------
+    %(input)s
+    derivative2 : callable
+        Callable with the following signature::
+
+            derivative2(input, axis, output, mode, cval,
+                        *extra_arguments, **extra_keywords)
+
+        See `extra_arguments`, `extra_keywords` below.
+    %(output)s
+    %(mode_multiple)s
+    %(cval)s
+    %(extra_keywords)s
+    %(extra_arguments)s
+
+    Returns
+    -------
+    generic_laplace : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    """
+    if extra_keywords is None:
+        extra_keywords = {}
+    input = np.asarray(input)
+    output = _ni_support._get_output(output, input)
+    axes = list(range(input.ndim))
+    if len(axes) > 0:
+        modes = _ni_support._normalize_sequence(mode, len(axes))
+        derivative2(input, axes[0], output, modes[0], cval,
+                    *extra_arguments, **extra_keywords)
+        for ii in range(1, len(axes)):
+            tmp = derivative2(input, axes[ii], output.dtype, modes[ii], cval,
+                              *extra_arguments, **extra_keywords)
+            output += tmp
+    else:
+        output[...] = input[...]
+    return output
+
+
+@_ni_docstrings.docfiller
+def laplace(input, output=None, mode="reflect", cval=0.0):
+    """N-D Laplace filter based on approximate second derivatives.
+
+    Parameters
+    ----------
+    %(input)s
+    %(output)s
+    %(mode_multiple)s
+    %(cval)s
+
+    Returns
+    -------
+    laplace : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> fig = plt.figure()
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ax1 = fig.add_subplot(121)  # left side
+    >>> ax2 = fig.add_subplot(122)  # right side
+    >>> ascent = datasets.ascent()
+    >>> result = ndimage.laplace(ascent)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result)
+    >>> plt.show()
+    """
+    def derivative2(input, axis, output, mode, cval):
+        return correlate1d(input, [1, -2, 1], axis, output, mode, cval, 0)
+    return generic_laplace(input, derivative2, output, mode, cval)
+
+
+@_ni_docstrings.docfiller
+def gaussian_laplace(input, sigma, output=None, mode="reflect",
+                     cval=0.0, **kwargs):
+    """Multidimensional Laplace filter using Gaussian second derivatives.
+
+    Parameters
+    ----------
+    %(input)s
+    sigma : scalar or sequence of scalars
+        The standard deviations of the Gaussian filter are given for
+        each axis as a sequence, or as a single number, in which case
+        it is equal for all axes.
+    %(output)s
+    %(mode_multiple)s
+    %(cval)s
+    Extra keyword arguments will be passed to gaussian_filter().
+
+    Returns
+    -------
+    gaussian_laplace : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> ascent = datasets.ascent()
+
+    >>> fig = plt.figure()
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ax1 = fig.add_subplot(121)  # left side
+    >>> ax2 = fig.add_subplot(122)  # right side
+
+    >>> result = ndimage.gaussian_laplace(ascent, sigma=1)
+    >>> ax1.imshow(result)
+
+    >>> result = ndimage.gaussian_laplace(ascent, sigma=3)
+    >>> ax2.imshow(result)
+    >>> plt.show()
+    """
+    input = np.asarray(input)
+
+    def derivative2(input, axis, output, mode, cval, sigma, **kwargs):
+        order = [0] * input.ndim
+        order[axis] = 2
+        return gaussian_filter(input, sigma, order, output, mode, cval,
+                               **kwargs)
+
+    return generic_laplace(input, derivative2, output, mode, cval,
+                           extra_arguments=(sigma,),
+                           extra_keywords=kwargs)
+
+
+@_ni_docstrings.docfiller
+def generic_gradient_magnitude(input, derivative, output=None,
+                               mode="reflect", cval=0.0,
+                               extra_arguments=(), extra_keywords=None):
+    """Gradient magnitude using a provided gradient function.
+
+    Parameters
+    ----------
+    %(input)s
+    derivative : callable
+        Callable with the following signature::
+
+            derivative(input, axis, output, mode, cval,
+                       *extra_arguments, **extra_keywords)
+
+        See `extra_arguments`, `extra_keywords` below.
+        `derivative` can assume that `input` and `output` are ndarrays.
+        Note that the output from `derivative` is modified inplace;
+        be careful to copy important inputs before returning them.
+    %(output)s
+    %(mode_multiple)s
+    %(cval)s
+    %(extra_keywords)s
+    %(extra_arguments)s
+
+    Returns
+    -------
+    generic_gradient_matnitude : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    """
+    if extra_keywords is None:
+        extra_keywords = {}
+    input = np.asarray(input)
+    output = _ni_support._get_output(output, input)
+    axes = list(range(input.ndim))
+    if len(axes) > 0:
+        modes = _ni_support._normalize_sequence(mode, len(axes))
+        derivative(input, axes[0], output, modes[0], cval,
+                   *extra_arguments, **extra_keywords)
+        np.multiply(output, output, output)
+        for ii in range(1, len(axes)):
+            tmp = derivative(input, axes[ii], output.dtype, modes[ii], cval,
+                             *extra_arguments, **extra_keywords)
+            np.multiply(tmp, tmp, tmp)
+            output += tmp
+        # This allows the sqrt to work with a different default casting
+        np.sqrt(output, output, casting='unsafe')
+    else:
+        output[...] = input[...]
+    return output
+
+
+@_ni_docstrings.docfiller
+def gaussian_gradient_magnitude(input, sigma, output=None,
+                                mode="reflect", cval=0.0, **kwargs):
+    """Multidimensional gradient magnitude using Gaussian derivatives.
+
+    Parameters
+    ----------
+    %(input)s
+    sigma : scalar or sequence of scalars
+        The standard deviations of the Gaussian filter are given for
+        each axis as a sequence, or as a single number, in which case
+        it is equal for all axes.
+    %(output)s
+    %(mode_multiple)s
+    %(cval)s
+    Extra keyword arguments will be passed to gaussian_filter().
+
+    Returns
+    -------
+    gaussian_gradient_magnitude : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> fig = plt.figure()
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ax1 = fig.add_subplot(121)  # left side
+    >>> ax2 = fig.add_subplot(122)  # right side
+    >>> ascent = datasets.ascent()
+    >>> result = ndimage.gaussian_gradient_magnitude(ascent, sigma=5)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result)
+    >>> plt.show()
+    """
+    input = np.asarray(input)
+
+    def derivative(input, axis, output, mode, cval, sigma, **kwargs):
+        order = [0] * input.ndim
+        order[axis] = 1
+        return gaussian_filter(input, sigma, order, output, mode,
+                               cval, **kwargs)
+
+    return generic_gradient_magnitude(input, derivative, output, mode,
+                                      cval, extra_arguments=(sigma,),
+                                      extra_keywords=kwargs)
+
+
+def _correlate_or_convolve(input, weights, output, mode, cval, origin,
+                           convolution):
+    input = np.asarray(input)
+    weights = np.asarray(weights)
+    complex_input = input.dtype.kind == 'c'
+    complex_weights = weights.dtype.kind == 'c'
+    if complex_input or complex_weights:
+        if complex_weights and not convolution:
+            # As for np.correlate, conjugate weights rather than input.
+            weights = weights.conj()
+        kwargs = dict(
+            mode=mode, origin=origin, convolution=convolution
+        )
+        output = _ni_support._get_output(output, input, complex_output=True)
+
+        return _complex_via_real_components(_correlate_or_convolve, input,
+                                            weights, output, cval, **kwargs)
+
+    origins = _ni_support._normalize_sequence(origin, input.ndim)
+    weights = np.asarray(weights, dtype=np.float64)
+    wshape = [ii for ii in weights.shape if ii > 0]
+    if len(wshape) != input.ndim:
+        raise RuntimeError('filter weights array has incorrect shape.')
+    if convolution:
+        weights = weights[tuple([slice(None, None, -1)] * weights.ndim)]
+        for ii in range(len(origins)):
+            origins[ii] = -origins[ii]
+            if not weights.shape[ii] & 1:
+                origins[ii] -= 1
+    for origin, lenw in zip(origins, wshape):
+        if _invalid_origin(origin, lenw):
+            raise ValueError('Invalid origin; origin must satisfy '
+                             '-(weights.shape[k] // 2) <= origin[k] <= '
+                             '(weights.shape[k]-1) // 2')
+
+    if not weights.flags.contiguous:
+        weights = weights.copy()
+    output = _ni_support._get_output(output, input)
+    temp_needed = np.may_share_memory(input, output)
+    if temp_needed:
+        # input and output arrays cannot share memory
+        temp = output
+        output = _ni_support._get_output(output.dtype, input)
+    if not isinstance(mode, str) and isinstance(mode, Iterable):
+        raise RuntimeError("A sequence of modes is not supported")
+    mode = _ni_support._extend_mode_to_code(mode)
+    _nd_image.correlate(input, weights, output, mode, cval, origins)
+    if temp_needed:
+        temp[...] = output
+        output = temp
+    return output
+
+
+@_ni_docstrings.docfiller
+def correlate(input, weights, output=None, mode='reflect', cval=0.0,
+              origin=0):
+    """
+    Multidimensional correlation.
+
+    The array is correlated with the given kernel.
+
+    Parameters
+    ----------
+    %(input)s
+    weights : ndarray
+        array of weights, same number of dimensions as input
+    %(output)s
+    %(mode_reflect)s
+    %(cval)s
+    %(origin_multiple)s
+
+    Returns
+    -------
+    result : ndarray
+        The result of correlation of `input` with `weights`.
+
+    See Also
+    --------
+    convolve : Convolve an image with a kernel.
+
+    Examples
+    --------
+    Correlation is the process of moving a filter mask often referred to
+    as kernel over the image and computing the sum of products at each location.
+
+    >>> from scipy.ndimage import correlate
+    >>> import numpy as np
+    >>> input_img = np.arange(25).reshape(5,5)
+    >>> print(input_img)
+    [[ 0  1  2  3  4]
+    [ 5  6  7  8  9]
+    [10 11 12 13 14]
+    [15 16 17 18 19]
+    [20 21 22 23 24]]
+
+    Define a kernel (weights) for correlation. In this example, it is for sum of
+    center and up, down, left and right next elements.
+
+    >>> weights = [[0, 1, 0],
+    ...            [1, 1, 1],
+    ...            [0, 1, 0]]
+
+    We can calculate a correlation result:
+    For example, element ``[2,2]`` is ``7 + 11 + 12 + 13 + 17 = 60``.
+
+    >>> correlate(input_img, weights)
+    array([[  6,  10,  15,  20,  24],
+        [ 26,  30,  35,  40,  44],
+        [ 51,  55,  60,  65,  69],
+        [ 76,  80,  85,  90,  94],
+        [ 96, 100, 105, 110, 114]])
+
+    """
+    return _correlate_or_convolve(input, weights, output, mode, cval,
+                                  origin, False)
+
+
+@_ni_docstrings.docfiller
+def convolve(input, weights, output=None, mode='reflect', cval=0.0,
+             origin=0):
+    """
+    Multidimensional convolution.
+
+    The array is convolved with the given kernel.
+
+    Parameters
+    ----------
+    %(input)s
+    weights : array_like
+        Array of weights, same number of dimensions as input
+    %(output)s
+    %(mode_reflect)s
+    cval : scalar, optional
+        Value to fill past edges of input if `mode` is 'constant'. Default
+        is 0.0
+    origin : int, optional
+        Controls the origin of the input signal, which is where the
+        filter is centered to produce the first element of the output.
+        Positive values shift the filter to the right, and negative values
+        shift the filter to the left. Default is 0.
+
+    Returns
+    -------
+    result : ndarray
+        The result of convolution of `input` with `weights`.
+
+    See Also
+    --------
+    correlate : Correlate an image with a kernel.
+
+    Notes
+    -----
+    Each value in result is :math:`C_i = \\sum_j{I_{i+k-j} W_j}`, where
+    W is the `weights` kernel,
+    j is the N-D spatial index over :math:`W`,
+    I is the `input` and k is the coordinate of the center of
+    W, specified by `origin` in the input parameters.
+
+    Examples
+    --------
+    Perhaps the simplest case to understand is ``mode='constant', cval=0.0``,
+    because in this case borders (i.e., where the `weights` kernel, centered
+    on any one value, extends beyond an edge of `input`) are treated as zeros.
+
+    >>> import numpy as np
+    >>> a = np.array([[1, 2, 0, 0],
+    ...               [5, 3, 0, 4],
+    ...               [0, 0, 0, 7],
+    ...               [9, 3, 0, 0]])
+    >>> k = np.array([[1,1,1],[1,1,0],[1,0,0]])
+    >>> from scipy import ndimage
+    >>> ndimage.convolve(a, k, mode='constant', cval=0.0)
+    array([[11, 10,  7,  4],
+           [10,  3, 11, 11],
+           [15, 12, 14,  7],
+           [12,  3,  7,  0]])
+
+    Setting ``cval=1.0`` is equivalent to padding the outer edge of `input`
+    with 1.0's (and then extracting only the original region of the result).
+
+    >>> ndimage.convolve(a, k, mode='constant', cval=1.0)
+    array([[13, 11,  8,  7],
+           [11,  3, 11, 14],
+           [16, 12, 14, 10],
+           [15,  6, 10,  5]])
+
+    With ``mode='reflect'`` (the default), outer values are reflected at the
+    edge of `input` to fill in missing values.
+
+    >>> b = np.array([[2, 0, 0],
+    ...               [1, 0, 0],
+    ...               [0, 0, 0]])
+    >>> k = np.array([[0,1,0], [0,1,0], [0,1,0]])
+    >>> ndimage.convolve(b, k, mode='reflect')
+    array([[5, 0, 0],
+           [3, 0, 0],
+           [1, 0, 0]])
+
+    This includes diagonally at the corners.
+
+    >>> k = np.array([[1,0,0],[0,1,0],[0,0,1]])
+    >>> ndimage.convolve(b, k)
+    array([[4, 2, 0],
+           [3, 2, 0],
+           [1, 1, 0]])
+
+    With ``mode='nearest'``, the single nearest value in to an edge in
+    `input` is repeated as many times as needed to match the overlapping
+    `weights`.
+
+    >>> c = np.array([[2, 0, 1],
+    ...               [1, 0, 0],
+    ...               [0, 0, 0]])
+    >>> k = np.array([[0, 1, 0],
+    ...               [0, 1, 0],
+    ...               [0, 1, 0],
+    ...               [0, 1, 0],
+    ...               [0, 1, 0]])
+    >>> ndimage.convolve(c, k, mode='nearest')
+    array([[7, 0, 3],
+           [5, 0, 2],
+           [3, 0, 1]])
+
+    """
+    return _correlate_or_convolve(input, weights, output, mode, cval,
+                                  origin, True)
+
+
+@_ni_docstrings.docfiller
+def uniform_filter1d(input, size, axis=-1, output=None,
+                     mode="reflect", cval=0.0, origin=0):
+    """Calculate a 1-D uniform filter along the given axis.
+
+    The lines of the array along the given axis are filtered with a
+    uniform filter of given size.
+
+    Parameters
+    ----------
+    %(input)s
+    size : int
+        length of uniform filter
+    %(axis)s
+    %(output)s
+    %(mode_reflect)s
+    %(cval)s
+    %(origin)s
+
+    Returns
+    -------
+    result : ndarray
+        Filtered array. Has same shape as `input`.
+
+    Examples
+    --------
+    >>> from scipy.ndimage import uniform_filter1d
+    >>> uniform_filter1d([2, 8, 0, 4, 1, 9, 9, 0], size=3)
+    array([4, 3, 4, 1, 4, 6, 6, 3])
+    """
+    input = np.asarray(input)
+    axis = normalize_axis_index(axis, input.ndim)
+    if size < 1:
+        raise RuntimeError('incorrect filter size')
+    complex_output = input.dtype.kind == 'c'
+    output = _ni_support._get_output(output, input,
+                                     complex_output=complex_output)
+    if (size // 2 + origin < 0) or (size // 2 + origin >= size):
+        raise ValueError('invalid origin')
+    mode = _ni_support._extend_mode_to_code(mode)
+    if not complex_output:
+        _nd_image.uniform_filter1d(input, size, axis, output, mode, cval,
+                                   origin)
+    else:
+        _nd_image.uniform_filter1d(input.real, size, axis, output.real, mode,
+                                   np.real(cval), origin)
+        _nd_image.uniform_filter1d(input.imag, size, axis, output.imag, mode,
+                                   np.imag(cval), origin)
+    return output
+
+
+@_ni_docstrings.docfiller
+def uniform_filter(input, size=3, output=None, mode="reflect",
+                   cval=0.0, origin=0, *, axes=None):
+    """Multidimensional uniform filter.
+
+    Parameters
+    ----------
+    %(input)s
+    size : int or sequence of ints, optional
+        The sizes of the uniform filter are given for each axis as a
+        sequence, or as a single number, in which case the size is
+        equal for all axes.
+    %(output)s
+    %(mode_multiple)s
+    %(cval)s
+    %(origin_multiple)s
+    axes : tuple of int or None, optional
+        If None, `input` is filtered along all axes. Otherwise,
+        `input` is filtered along the specified axes. When `axes` is
+        specified, any tuples used for `size`, `origin`, and/or `mode`
+        must match the length of `axes`. The ith entry in any of these tuples
+        corresponds to the ith entry in `axes`.
+
+    Returns
+    -------
+    uniform_filter : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    Notes
+    -----
+    The multidimensional filter is implemented as a sequence of
+    1-D uniform filters. The intermediate arrays are stored
+    in the same data type as the output. Therefore, for output types
+    with a limited precision, the results may be imprecise because
+    intermediate results may be stored with insufficient precision.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> fig = plt.figure()
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ax1 = fig.add_subplot(121)  # left side
+    >>> ax2 = fig.add_subplot(122)  # right side
+    >>> ascent = datasets.ascent()
+    >>> result = ndimage.uniform_filter(ascent, size=20)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result)
+    >>> plt.show()
+    """
+    input = np.asarray(input)
+    output = _ni_support._get_output(output, input,
+                                     complex_output=input.dtype.kind == 'c')
+    axes = _ni_support._check_axes(axes, input.ndim)
+    num_axes = len(axes)
+    sizes = _ni_support._normalize_sequence(size, num_axes)
+    origins = _ni_support._normalize_sequence(origin, num_axes)
+    modes = _ni_support._normalize_sequence(mode, num_axes)
+    axes = [(axes[ii], sizes[ii], origins[ii], modes[ii])
+            for ii in range(num_axes) if sizes[ii] > 1]
+    if len(axes) > 0:
+        for axis, size, origin, mode in axes:
+            uniform_filter1d(input, int(size), axis, output, mode,
+                             cval, origin)
+            input = output
+    else:
+        output[...] = input[...]
+    return output
+
+
+@_ni_docstrings.docfiller
+def minimum_filter1d(input, size, axis=-1, output=None,
+                     mode="reflect", cval=0.0, origin=0):
+    """Calculate a 1-D minimum filter along the given axis.
+
+    The lines of the array along the given axis are filtered with a
+    minimum filter of given size.
+
+    Parameters
+    ----------
+    %(input)s
+    size : int
+        length along which to calculate 1D minimum
+    %(axis)s
+    %(output)s
+    %(mode_reflect)s
+    %(cval)s
+    %(origin)s
+
+    Returns
+    -------
+    result : ndarray.
+        Filtered image. Has the same shape as `input`.
+
+    Notes
+    -----
+    This function implements the MINLIST algorithm [1]_, as described by
+    Richard Harter [2]_, and has a guaranteed O(n) performance, `n` being
+    the `input` length, regardless of filter size.
+
+    References
+    ----------
+    .. [1] http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.42.2777
+    .. [2] http://www.richardhartersworld.com/cri/2001/slidingmin.html
+
+
+    Examples
+    --------
+    >>> from scipy.ndimage import minimum_filter1d
+    >>> minimum_filter1d([2, 8, 0, 4, 1, 9, 9, 0], size=3)
+    array([2, 0, 0, 0, 1, 1, 0, 0])
+    """
+    input = np.asarray(input)
+    if np.iscomplexobj(input):
+        raise TypeError('Complex type not supported')
+    axis = normalize_axis_index(axis, input.ndim)
+    if size < 1:
+        raise RuntimeError('incorrect filter size')
+    output = _ni_support._get_output(output, input)
+    if (size // 2 + origin < 0) or (size // 2 + origin >= size):
+        raise ValueError('invalid origin')
+    mode = _ni_support._extend_mode_to_code(mode)
+    _nd_image.min_or_max_filter1d(input, size, axis, output, mode, cval,
+                                  origin, 1)
+    return output
+
+
+@_ni_docstrings.docfiller
+def maximum_filter1d(input, size, axis=-1, output=None,
+                     mode="reflect", cval=0.0, origin=0):
+    """Calculate a 1-D maximum filter along the given axis.
+
+    The lines of the array along the given axis are filtered with a
+    maximum filter of given size.
+
+    Parameters
+    ----------
+    %(input)s
+    size : int
+        Length along which to calculate the 1-D maximum.
+    %(axis)s
+    %(output)s
+    %(mode_reflect)s
+    %(cval)s
+    %(origin)s
+
+    Returns
+    -------
+    maximum1d : ndarray, None
+        Maximum-filtered array with same shape as input.
+        None if `output` is not None
+
+    Notes
+    -----
+    This function implements the MAXLIST algorithm [1]_, as described by
+    Richard Harter [2]_, and has a guaranteed O(n) performance, `n` being
+    the `input` length, regardless of filter size.
+
+    References
+    ----------
+    .. [1] http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.42.2777
+    .. [2] http://www.richardhartersworld.com/cri/2001/slidingmin.html
+
+    Examples
+    --------
+    >>> from scipy.ndimage import maximum_filter1d
+    >>> maximum_filter1d([2, 8, 0, 4, 1, 9, 9, 0], size=3)
+    array([8, 8, 8, 4, 9, 9, 9, 9])
+    """
+    input = np.asarray(input)
+    if np.iscomplexobj(input):
+        raise TypeError('Complex type not supported')
+    axis = normalize_axis_index(axis, input.ndim)
+    if size < 1:
+        raise RuntimeError('incorrect filter size')
+    output = _ni_support._get_output(output, input)
+    if (size // 2 + origin < 0) or (size // 2 + origin >= size):
+        raise ValueError('invalid origin')
+    mode = _ni_support._extend_mode_to_code(mode)
+    _nd_image.min_or_max_filter1d(input, size, axis, output, mode, cval,
+                                  origin, 0)
+    return output
+
+
+def _min_or_max_filter(input, size, footprint, structure, output, mode,
+                       cval, origin, minimum, axes=None):
+    if (size is not None) and (footprint is not None):
+        warnings.warn("ignoring size because footprint is set",
+                      UserWarning, stacklevel=3)
+    if structure is None:
+        if footprint is None:
+            if size is None:
+                raise RuntimeError("no footprint provided")
+            separable = True
+        else:
+            footprint = np.asarray(footprint, dtype=bool)
+            if not footprint.any():
+                raise ValueError("All-zero footprint is not supported.")
+            if footprint.all():
+                size = footprint.shape
+                footprint = None
+                separable = True
+            else:
+                separable = False
+    else:
+        structure = np.asarray(structure, dtype=np.float64)
+        separable = False
+        if footprint is None:
+            footprint = np.ones(structure.shape, bool)
+        else:
+            footprint = np.asarray(footprint, dtype=bool)
+    input = np.asarray(input)
+    if np.iscomplexobj(input):
+        raise TypeError("Complex type not supported")
+    output = _ni_support._get_output(output, input)
+    temp_needed = np.may_share_memory(input, output)
+    if temp_needed:
+        # input and output arrays cannot share memory
+        temp = output
+        output = _ni_support._get_output(output.dtype, input)
+    axes = _ni_support._check_axes(axes, input.ndim)
+    num_axes = len(axes)
+    if separable:
+        origins = _ni_support._normalize_sequence(origin, num_axes)
+        sizes = _ni_support._normalize_sequence(size, num_axes)
+        modes = _ni_support._normalize_sequence(mode, num_axes)
+        axes = [(axes[ii], sizes[ii], origins[ii], modes[ii])
+                for ii in range(len(axes)) if sizes[ii] > 1]
+        if minimum:
+            filter_ = minimum_filter1d
+        else:
+            filter_ = maximum_filter1d
+        if len(axes) > 0:
+            for axis, size, origin, mode in axes:
+                filter_(input, int(size), axis, output, mode, cval, origin)
+                input = output
+        else:
+            output[...] = input[...]
+    else:
+        origins = _ni_support._normalize_sequence(origin, num_axes)
+        if num_axes < input.ndim:
+            if footprint.ndim != num_axes:
+                raise RuntimeError("footprint array has incorrect shape")
+            footprint = np.expand_dims(
+                footprint,
+                tuple(ax for ax in range(input.ndim) if ax not in axes)
+            )
+            # set origin = 0 for any axes not being filtered
+            origins_temp = [0,] * input.ndim
+            for o, ax in zip(origins, axes):
+                origins_temp[ax] = o
+            origins = origins_temp
+
+        fshape = [ii for ii in footprint.shape if ii > 0]
+        if len(fshape) != input.ndim:
+            raise RuntimeError('footprint array has incorrect shape.')
+        for origin, lenf in zip(origins, fshape):
+            if (lenf // 2 + origin < 0) or (lenf // 2 + origin >= lenf):
+                raise ValueError("invalid origin")
+        if not footprint.flags.contiguous:
+            footprint = footprint.copy()
+        if structure is not None:
+            if len(structure.shape) != input.ndim:
+                raise RuntimeError("structure array has incorrect shape")
+            if num_axes != structure.ndim:
+                structure = np.expand_dims(
+                    structure,
+                    tuple(ax for ax in range(structure.ndim) if ax not in axes)
+                )
+            if not structure.flags.contiguous:
+                structure = structure.copy()
+        if not isinstance(mode, str) and isinstance(mode, Iterable):
+            raise RuntimeError(
+                "A sequence of modes is not supported for non-separable "
+                "footprints")
+        mode = _ni_support._extend_mode_to_code(mode)
+        _nd_image.min_or_max_filter(input, footprint, structure, output,
+                                    mode, cval, origins, minimum)
+    if temp_needed:
+        temp[...] = output
+        output = temp
+    return output
+
+
+@_ni_docstrings.docfiller
+def minimum_filter(input, size=None, footprint=None, output=None,
+                   mode="reflect", cval=0.0, origin=0, *, axes=None):
+    """Calculate a multidimensional minimum filter.
+
+    Parameters
+    ----------
+    %(input)s
+    %(size_foot)s
+    %(output)s
+    %(mode_multiple)s
+    %(cval)s
+    %(origin_multiple)s
+    axes : tuple of int or None, optional
+        If None, `input` is filtered along all axes. Otherwise,
+        `input` is filtered along the specified axes. When `axes` is
+        specified, any tuples used for `size`, `origin`, and/or `mode`
+        must match the length of `axes`. The ith entry in any of these tuples
+        corresponds to the ith entry in `axes`.
+
+    Returns
+    -------
+    minimum_filter : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    Notes
+    -----
+    A sequence of modes (one per axis) is only supported when the footprint is
+    separable. Otherwise, a single mode string must be provided.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> fig = plt.figure()
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ax1 = fig.add_subplot(121)  # left side
+    >>> ax2 = fig.add_subplot(122)  # right side
+    >>> ascent = datasets.ascent()
+    >>> result = ndimage.minimum_filter(ascent, size=20)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result)
+    >>> plt.show()
+    """
+    return _min_or_max_filter(input, size, footprint, None, output, mode,
+                              cval, origin, 1, axes)
+
+
+@_ni_docstrings.docfiller
+def maximum_filter(input, size=None, footprint=None, output=None,
+                   mode="reflect", cval=0.0, origin=0, *, axes=None):
+    """Calculate a multidimensional maximum filter.
+
+    Parameters
+    ----------
+    %(input)s
+    %(size_foot)s
+    %(output)s
+    %(mode_multiple)s
+    %(cval)s
+    %(origin_multiple)s
+    axes : tuple of int or None, optional
+        If None, `input` is filtered along all axes. Otherwise,
+        `input` is filtered along the specified axes. When `axes` is
+        specified, any tuples used for `size`, `origin`, and/or `mode`
+        must match the length of `axes`. The ith entry in any of these tuples
+        corresponds to the ith entry in `axes`.
+
+    Returns
+    -------
+    maximum_filter : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    Notes
+    -----
+    A sequence of modes (one per axis) is only supported when the footprint is
+    separable. Otherwise, a single mode string must be provided.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> fig = plt.figure()
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ax1 = fig.add_subplot(121)  # left side
+    >>> ax2 = fig.add_subplot(122)  # right side
+    >>> ascent = datasets.ascent()
+    >>> result = ndimage.maximum_filter(ascent, size=20)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result)
+    >>> plt.show()
+    """
+    return _min_or_max_filter(input, size, footprint, None, output, mode,
+                              cval, origin, 0, axes)
+
+
+@_ni_docstrings.docfiller
+def _rank_filter(input, rank, size=None, footprint=None, output=None,
+                 mode="reflect", cval=0.0, origin=0, operation='rank',
+                 axes=None):
+    if (size is not None) and (footprint is not None):
+        warnings.warn("ignoring size because footprint is set",
+                      UserWarning, stacklevel=3)
+    input = np.asarray(input)
+    if np.iscomplexobj(input):
+        raise TypeError('Complex type not supported')
+    axes = _ni_support._check_axes(axes, input.ndim)
+    num_axes = len(axes)
+    origins = _ni_support._normalize_sequence(origin, num_axes)
+    if footprint is None:
+        if size is None:
+            raise RuntimeError("no footprint or filter size provided")
+        sizes = _ni_support._normalize_sequence(size, num_axes)
+        footprint = np.ones(sizes, dtype=bool)
+    else:
+        footprint = np.asarray(footprint, dtype=bool)
+    if num_axes < input.ndim:
+        # set origin = 0 for any axes not being filtered
+        origins_temp = [0,] * input.ndim
+        for o, ax in zip(origins, axes):
+            origins_temp[ax] = o
+        origins = origins_temp
+
+        if not isinstance(mode, str) and isinstance(mode, Iterable):
+            # set mode = 'constant' for any axes not being filtered
+            modes = _ni_support._normalize_sequence(mode, num_axes)
+            modes_temp = ['constant'] * input.ndim
+            for m, ax in zip(modes, axes):
+                modes_temp[ax] = m
+            mode = modes_temp
+
+        # insert singleton dimension along any non-filtered axes
+        if footprint.ndim != num_axes:
+            raise RuntimeError("footprint array has incorrect shape")
+        footprint = np.expand_dims(
+            footprint,
+            tuple(ax for ax in range(input.ndim) if ax not in axes)
+        )
+    fshape = [ii for ii in footprint.shape if ii > 0]
+    if len(fshape) != input.ndim:
+        raise RuntimeError('footprint array has incorrect shape.')
+    for origin, lenf in zip(origins, fshape):
+        if (lenf // 2 + origin < 0) or (lenf // 2 + origin >= lenf):
+            raise ValueError('invalid origin')
+    if not footprint.flags.contiguous:
+        footprint = footprint.copy()
+    filter_size = np.where(footprint, 1, 0).sum()
+    if operation == 'median':
+        rank = filter_size // 2
+    elif operation == 'percentile':
+        percentile = rank
+        if percentile < 0.0:
+            percentile += 100.0
+        if percentile < 0 or percentile > 100:
+            raise RuntimeError('invalid percentile')
+        if percentile == 100.0:
+            rank = filter_size - 1
+        else:
+            rank = int(float(filter_size) * percentile / 100.0)
+    if rank < 0:
+        rank += filter_size
+    if rank < 0 or rank >= filter_size:
+        raise RuntimeError('rank not within filter footprint size')
+    if rank == 0:
+        return minimum_filter(input, None, footprint, output, mode, cval,
+                              origins, axes=None)
+    elif rank == filter_size - 1:
+        return maximum_filter(input, None, footprint, output, mode, cval,
+                              origins, axes=None)
+    else:
+        output = _ni_support._get_output(output, input)
+        temp_needed = np.may_share_memory(input, output)
+        if temp_needed:
+            # input and output arrays cannot share memory
+            temp = output
+            output = _ni_support._get_output(output.dtype, input)
+        if not isinstance(mode, str) and isinstance(mode, Iterable):
+            raise RuntimeError(
+                "A sequence of modes is not supported by non-separable rank "
+                "filters")
+        mode = _ni_support._extend_mode_to_code(mode)
+        _nd_image.rank_filter(input, rank, footprint, output, mode, cval,
+                              origins)
+        if temp_needed:
+            temp[...] = output
+            output = temp
+        return output
+
+
+@_ni_docstrings.docfiller
+def rank_filter(input, rank, size=None, footprint=None, output=None,
+                mode="reflect", cval=0.0, origin=0, *, axes=None):
+    """Calculate a multidimensional rank filter.
+
+    Parameters
+    ----------
+    %(input)s
+    rank : int
+        The rank parameter may be less than zero, i.e., rank = -1
+        indicates the largest element.
+    %(size_foot)s
+    %(output)s
+    %(mode_reflect)s
+    %(cval)s
+    %(origin_multiple)s
+    axes : tuple of int or None, optional
+        If None, `input` is filtered along all axes. Otherwise,
+        `input` is filtered along the specified axes.
+
+    Returns
+    -------
+    rank_filter : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> fig = plt.figure()
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ax1 = fig.add_subplot(121)  # left side
+    >>> ax2 = fig.add_subplot(122)  # right side
+    >>> ascent = datasets.ascent()
+    >>> result = ndimage.rank_filter(ascent, rank=42, size=20)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result)
+    >>> plt.show()
+    """
+    rank = operator.index(rank)
+    return _rank_filter(input, rank, size, footprint, output, mode, cval,
+                        origin, 'rank', axes=axes)
+
+
+@_ni_docstrings.docfiller
+def median_filter(input, size=None, footprint=None, output=None,
+                  mode="reflect", cval=0.0, origin=0, *, axes=None):
+    """
+    Calculate a multidimensional median filter.
+
+    Parameters
+    ----------
+    %(input)s
+    %(size_foot)s
+    %(output)s
+    %(mode_reflect)s
+    %(cval)s
+    %(origin_multiple)s
+    axes : tuple of int or None, optional
+        If None, `input` is filtered along all axes. Otherwise,
+        `input` is filtered along the specified axes.
+
+    Returns
+    -------
+    median_filter : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    See Also
+    --------
+    scipy.signal.medfilt2d
+
+    Notes
+    -----
+    For 2-dimensional images with ``uint8``, ``float32`` or ``float64`` dtypes
+    the specialised function `scipy.signal.medfilt2d` may be faster. It is
+    however limited to constant mode with ``cval=0``.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> fig = plt.figure()
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ax1 = fig.add_subplot(121)  # left side
+    >>> ax2 = fig.add_subplot(122)  # right side
+    >>> ascent = datasets.ascent()
+    >>> result = ndimage.median_filter(ascent, size=20)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result)
+    >>> plt.show()
+    """
+    return _rank_filter(input, 0, size, footprint, output, mode, cval,
+                        origin, 'median', axes=axes)
+
+
+@_ni_docstrings.docfiller
+def percentile_filter(input, percentile, size=None, footprint=None,
+                      output=None, mode="reflect", cval=0.0, origin=0, *,
+                      axes=None):
+    """Calculate a multidimensional percentile filter.
+
+    Parameters
+    ----------
+    %(input)s
+    percentile : scalar
+        The percentile parameter may be less than zero, i.e.,
+        percentile = -20 equals percentile = 80
+    %(size_foot)s
+    %(output)s
+    %(mode_reflect)s
+    %(cval)s
+    %(origin_multiple)s
+    axes : tuple of int or None, optional
+        If None, `input` is filtered along all axes. Otherwise,
+        `input` is filtered along the specified axes.
+
+    Returns
+    -------
+    percentile_filter : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> fig = plt.figure()
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ax1 = fig.add_subplot(121)  # left side
+    >>> ax2 = fig.add_subplot(122)  # right side
+    >>> ascent = datasets.ascent()
+    >>> result = ndimage.percentile_filter(ascent, percentile=20, size=20)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result)
+    >>> plt.show()
+    """
+    return _rank_filter(input, percentile, size, footprint, output, mode,
+                        cval, origin, 'percentile', axes=axes)
+
+
+@_ni_docstrings.docfiller
+def generic_filter1d(input, function, filter_size, axis=-1,
+                     output=None, mode="reflect", cval=0.0, origin=0,
+                     extra_arguments=(), extra_keywords=None):
+    """Calculate a 1-D filter along the given axis.
+
+    `generic_filter1d` iterates over the lines of the array, calling the
+    given function at each line. The arguments of the line are the
+    input line, and the output line. The input and output lines are 1-D
+    double arrays. The input line is extended appropriately according
+    to the filter size and origin. The output line must be modified
+    in-place with the result.
+
+    Parameters
+    ----------
+    %(input)s
+    function : {callable, scipy.LowLevelCallable}
+        Function to apply along given axis.
+    filter_size : scalar
+        Length of the filter.
+    %(axis)s
+    %(output)s
+    %(mode_reflect)s
+    %(cval)s
+    %(origin)s
+    %(extra_arguments)s
+    %(extra_keywords)s
+
+    Returns
+    -------
+    generic_filter1d : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    Notes
+    -----
+    This function also accepts low-level callback functions with one of
+    the following signatures and wrapped in `scipy.LowLevelCallable`:
+
+    .. code:: c
+
+       int function(double *input_line, npy_intp input_length,
+                    double *output_line, npy_intp output_length,
+                    void *user_data)
+       int function(double *input_line, intptr_t input_length,
+                    double *output_line, intptr_t output_length,
+                    void *user_data)
+
+    The calling function iterates over the lines of the input and output
+    arrays, calling the callback function at each line. The current line
+    is extended according to the border conditions set by the calling
+    function, and the result is copied into the array that is passed
+    through ``input_line``. The length of the input line (after extension)
+    is passed through ``input_length``. The callback function should apply
+    the filter and store the result in the array passed through
+    ``output_line``. The length of the output line is passed through
+    ``output_length``. ``user_data`` is the data pointer provided
+    to `scipy.LowLevelCallable` as-is.
+
+    The callback function must return an integer error status that is zero
+    if something went wrong and one otherwise. If an error occurs, you should
+    normally set the python error status with an informative message
+    before returning, otherwise a default error message is set by the
+    calling function.
+
+    In addition, some other low-level function pointer specifications
+    are accepted, but these are for backward compatibility only and should
+    not be used in new code.
+
+    """
+    if extra_keywords is None:
+        extra_keywords = {}
+    input = np.asarray(input)
+    if np.iscomplexobj(input):
+        raise TypeError('Complex type not supported')
+    output = _ni_support._get_output(output, input)
+    if filter_size < 1:
+        raise RuntimeError('invalid filter size')
+    axis = normalize_axis_index(axis, input.ndim)
+    if (filter_size // 2 + origin < 0) or (filter_size // 2 + origin >=
+                                           filter_size):
+        raise ValueError('invalid origin')
+    mode = _ni_support._extend_mode_to_code(mode)
+    _nd_image.generic_filter1d(input, function, filter_size, axis, output,
+                               mode, cval, origin, extra_arguments,
+                               extra_keywords)
+    return output
+
+
+@_ni_docstrings.docfiller
+def generic_filter(input, function, size=None, footprint=None,
+                   output=None, mode="reflect", cval=0.0, origin=0,
+                   extra_arguments=(), extra_keywords=None):
+    """Calculate a multidimensional filter using the given function.
+
+    At each element the provided function is called. The input values
+    within the filter footprint at that element are passed to the function
+    as a 1-D array of double values.
+
+    Parameters
+    ----------
+    %(input)s
+    function : {callable, scipy.LowLevelCallable}
+        Function to apply at each element.
+    %(size_foot)s
+    %(output)s
+    %(mode_reflect)s
+    %(cval)s
+    %(origin_multiple)s
+    %(extra_arguments)s
+    %(extra_keywords)s
+
+    Returns
+    -------
+    generic_filter : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    Notes
+    -----
+    This function also accepts low-level callback functions with one of
+    the following signatures and wrapped in `scipy.LowLevelCallable`:
+
+    .. code:: c
+
+       int callback(double *buffer, npy_intp filter_size,
+                    double *return_value, void *user_data)
+       int callback(double *buffer, intptr_t filter_size,
+                    double *return_value, void *user_data)
+
+    The calling function iterates over the elements of the input and
+    output arrays, calling the callback function at each element. The
+    elements within the footprint of the filter at the current element are
+    passed through the ``buffer`` parameter, and the number of elements
+    within the footprint through ``filter_size``. The calculated value is
+    returned in ``return_value``. ``user_data`` is the data pointer provided
+    to `scipy.LowLevelCallable` as-is.
+
+    The callback function must return an integer error status that is zero
+    if something went wrong and one otherwise. If an error occurs, you should
+    normally set the python error status with an informative message
+    before returning, otherwise a default error message is set by the
+    calling function.
+
+    In addition, some other low-level function pointer specifications
+    are accepted, but these are for backward compatibility only and should
+    not be used in new code.
+
+    Examples
+    --------
+    Import the necessary modules and load the example image used for
+    filtering.
+
+    >>> import numpy as np
+    >>> from scipy import datasets
+    >>> from scipy.ndimage import zoom, generic_filter
+    >>> import matplotlib.pyplot as plt
+    >>> ascent = zoom(datasets.ascent(), 0.5)
+
+    Compute a maximum filter with kernel size 5 by passing a simple NumPy
+    aggregation function as argument to `function`.
+
+    >>> maximum_filter_result = generic_filter(ascent, np.amax, [5, 5])
+
+    While a maximmum filter could also directly be obtained using
+    `maximum_filter`, `generic_filter` allows generic Python function or
+    `scipy.LowLevelCallable` to be used as a filter. Here, we compute the
+    range between maximum and minimum value as an example for a kernel size
+    of 5.
+
+    >>> def custom_filter(image):
+    ...     return np.amax(image) - np.amin(image)
+    >>> custom_filter_result = generic_filter(ascent, custom_filter, [5, 5])
+
+    Plot the original and filtered images.
+
+    >>> fig, axes = plt.subplots(3, 1, figsize=(3, 9))
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> top, middle, bottom = axes
+    >>> for ax in axes:
+    ...     ax.set_axis_off()  # remove coordinate system
+    >>> top.imshow(ascent)
+    >>> top.set_title("Original image")
+    >>> middle.imshow(maximum_filter_result)
+    >>> middle.set_title("Maximum filter, Kernel: 5x5")
+    >>> bottom.imshow(custom_filter_result)
+    >>> bottom.set_title("Custom filter, Kernel: 5x5")
+    >>> fig.tight_layout()
+
+    """
+    if (size is not None) and (footprint is not None):
+        warnings.warn("ignoring size because footprint is set",
+                      UserWarning, stacklevel=2)
+    if extra_keywords is None:
+        extra_keywords = {}
+    input = np.asarray(input)
+    if np.iscomplexobj(input):
+        raise TypeError('Complex type not supported')
+    origins = _ni_support._normalize_sequence(origin, input.ndim)
+    if footprint is None:
+        if size is None:
+            raise RuntimeError("no footprint or filter size provided")
+        sizes = _ni_support._normalize_sequence(size, input.ndim)
+        footprint = np.ones(sizes, dtype=bool)
+    else:
+        footprint = np.asarray(footprint, dtype=bool)
+    fshape = [ii for ii in footprint.shape if ii > 0]
+    if len(fshape) != input.ndim:
+        raise RuntimeError('filter footprint array has incorrect shape.')
+    for origin, lenf in zip(origins, fshape):
+        if (lenf // 2 + origin < 0) or (lenf // 2 + origin >= lenf):
+            raise ValueError('invalid origin')
+    if not footprint.flags.contiguous:
+        footprint = footprint.copy()
+    output = _ni_support._get_output(output, input)
+    mode = _ni_support._extend_mode_to_code(mode)
+    _nd_image.generic_filter(input, function, footprint, output, mode,
+                             cval, origins, extra_arguments, extra_keywords)
+    return output
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_fourier.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_fourier.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb5ffa6b9287cb740611aefba5f1f322011518cf
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_fourier.py
@@ -0,0 +1,306 @@
+# Copyright (C) 2003-2005 Peter J. Verveer
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above
+#    copyright notice, this list of conditions and the following
+#    disclaimer in the documentation and/or other materials provided
+#    with the distribution.
+#
+# 3. The name of the author may not be used to endorse or promote
+#    products derived from this software without specific prior
+#    written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import numpy as np
+from scipy._lib._util import normalize_axis_index
+from . import _ni_support
+from . import _nd_image
+
+__all__ = ['fourier_gaussian', 'fourier_uniform', 'fourier_ellipsoid',
+           'fourier_shift']
+
+
+def _get_output_fourier(output, input):
+    if output is None:
+        if input.dtype.type in [np.complex64, np.complex128, np.float32]:
+            output = np.zeros(input.shape, dtype=input.dtype)
+        else:
+            output = np.zeros(input.shape, dtype=np.float64)
+    elif type(output) is type:
+        if output not in [np.complex64, np.complex128,
+                          np.float32, np.float64]:
+            raise RuntimeError("output type not supported")
+        output = np.zeros(input.shape, dtype=output)
+    elif output.shape != input.shape:
+        raise RuntimeError("output shape not correct")
+    return output
+
+
+def _get_output_fourier_complex(output, input):
+    if output is None:
+        if input.dtype.type in [np.complex64, np.complex128]:
+            output = np.zeros(input.shape, dtype=input.dtype)
+        else:
+            output = np.zeros(input.shape, dtype=np.complex128)
+    elif type(output) is type:
+        if output not in [np.complex64, np.complex128]:
+            raise RuntimeError("output type not supported")
+        output = np.zeros(input.shape, dtype=output)
+    elif output.shape != input.shape:
+        raise RuntimeError("output shape not correct")
+    return output
+
+
+def fourier_gaussian(input, sigma, n=-1, axis=-1, output=None):
+    """
+    Multidimensional Gaussian fourier filter.
+
+    The array is multiplied with the fourier transform of a Gaussian
+    kernel.
+
+    Parameters
+    ----------
+    input : array_like
+        The input array.
+    sigma : float or sequence
+        The sigma of the Gaussian kernel. If a float, `sigma` is the same for
+        all axes. If a sequence, `sigma` has to contain one value for each
+        axis.
+    n : int, optional
+        If `n` is negative (default), then the input is assumed to be the
+        result of a complex fft.
+        If `n` is larger than or equal to zero, the input is assumed to be the
+        result of a real fft, and `n` gives the length of the array before
+        transformation along the real transform direction.
+    axis : int, optional
+        The axis of the real transform.
+    output : ndarray, optional
+        If given, the result of filtering the input is placed in this array.
+
+    Returns
+    -------
+    fourier_gaussian : ndarray
+        The filtered input.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import numpy.fft
+    >>> import matplotlib.pyplot as plt
+    >>> fig, (ax1, ax2) = plt.subplots(1, 2)
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ascent = datasets.ascent()
+    >>> input_ = numpy.fft.fft2(ascent)
+    >>> result = ndimage.fourier_gaussian(input_, sigma=4)
+    >>> result = numpy.fft.ifft2(result)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result.real)  # the imaginary part is an artifact
+    >>> plt.show()
+    """
+    input = np.asarray(input)
+    output = _get_output_fourier(output, input)
+    axis = normalize_axis_index(axis, input.ndim)
+    sigmas = _ni_support._normalize_sequence(sigma, input.ndim)
+    sigmas = np.asarray(sigmas, dtype=np.float64)
+    if not sigmas.flags.contiguous:
+        sigmas = sigmas.copy()
+
+    _nd_image.fourier_filter(input, sigmas, n, axis, output, 0)
+    return output
+
+
+def fourier_uniform(input, size, n=-1, axis=-1, output=None):
+    """
+    Multidimensional uniform fourier filter.
+
+    The array is multiplied with the Fourier transform of a box of given
+    size.
+
+    Parameters
+    ----------
+    input : array_like
+        The input array.
+    size : float or sequence
+        The size of the box used for filtering.
+        If a float, `size` is the same for all axes. If a sequence, `size` has
+        to contain one value for each axis.
+    n : int, optional
+        If `n` is negative (default), then the input is assumed to be the
+        result of a complex fft.
+        If `n` is larger than or equal to zero, the input is assumed to be the
+        result of a real fft, and `n` gives the length of the array before
+        transformation along the real transform direction.
+    axis : int, optional
+        The axis of the real transform.
+    output : ndarray, optional
+        If given, the result of filtering the input is placed in this array.
+
+    Returns
+    -------
+    fourier_uniform : ndarray
+        The filtered input.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import numpy.fft
+    >>> import matplotlib.pyplot as plt
+    >>> fig, (ax1, ax2) = plt.subplots(1, 2)
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ascent = datasets.ascent()
+    >>> input_ = numpy.fft.fft2(ascent)
+    >>> result = ndimage.fourier_uniform(input_, size=20)
+    >>> result = numpy.fft.ifft2(result)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result.real)  # the imaginary part is an artifact
+    >>> plt.show()
+    """
+    input = np.asarray(input)
+    output = _get_output_fourier(output, input)
+    axis = normalize_axis_index(axis, input.ndim)
+    sizes = _ni_support._normalize_sequence(size, input.ndim)
+    sizes = np.asarray(sizes, dtype=np.float64)
+    if not sizes.flags.contiguous:
+        sizes = sizes.copy()
+    _nd_image.fourier_filter(input, sizes, n, axis, output, 1)
+    return output
+
+
+def fourier_ellipsoid(input, size, n=-1, axis=-1, output=None):
+    """
+    Multidimensional ellipsoid Fourier filter.
+
+    The array is multiplied with the fourier transform of an ellipsoid of
+    given sizes.
+
+    Parameters
+    ----------
+    input : array_like
+        The input array.
+    size : float or sequence
+        The size of the box used for filtering.
+        If a float, `size` is the same for all axes. If a sequence, `size` has
+        to contain one value for each axis.
+    n : int, optional
+        If `n` is negative (default), then the input is assumed to be the
+        result of a complex fft.
+        If `n` is larger than or equal to zero, the input is assumed to be the
+        result of a real fft, and `n` gives the length of the array before
+        transformation along the real transform direction.
+    axis : int, optional
+        The axis of the real transform.
+    output : ndarray, optional
+        If given, the result of filtering the input is placed in this array.
+
+    Returns
+    -------
+    fourier_ellipsoid : ndarray
+        The filtered input.
+
+    Notes
+    -----
+    This function is implemented for arrays of rank 1, 2, or 3.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import numpy.fft
+    >>> import matplotlib.pyplot as plt
+    >>> fig, (ax1, ax2) = plt.subplots(1, 2)
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ascent = datasets.ascent()
+    >>> input_ = numpy.fft.fft2(ascent)
+    >>> result = ndimage.fourier_ellipsoid(input_, size=20)
+    >>> result = numpy.fft.ifft2(result)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result.real)  # the imaginary part is an artifact
+    >>> plt.show()
+    """
+    input = np.asarray(input)
+    if input.ndim > 3:
+        raise NotImplementedError("Only 1d, 2d and 3d inputs are supported")
+    output = _get_output_fourier(output, input)
+    if output.size == 0:
+        # The C code has a bug that can result in a segfault with arrays
+        # that have size 0 (gh-17270), so check here.
+        return output
+    axis = normalize_axis_index(axis, input.ndim)
+    sizes = _ni_support._normalize_sequence(size, input.ndim)
+    sizes = np.asarray(sizes, dtype=np.float64)
+    if not sizes.flags.contiguous:
+        sizes = sizes.copy()
+    _nd_image.fourier_filter(input, sizes, n, axis, output, 2)
+    return output
+
+
+def fourier_shift(input, shift, n=-1, axis=-1, output=None):
+    """
+    Multidimensional Fourier shift filter.
+
+    The array is multiplied with the Fourier transform of a shift operation.
+
+    Parameters
+    ----------
+    input : array_like
+        The input array.
+    shift : float or sequence
+        The size of the box used for filtering.
+        If a float, `shift` is the same for all axes. If a sequence, `shift`
+        has to contain one value for each axis.
+    n : int, optional
+        If `n` is negative (default), then the input is assumed to be the
+        result of a complex fft.
+        If `n` is larger than or equal to zero, the input is assumed to be the
+        result of a real fft, and `n` gives the length of the array before
+        transformation along the real transform direction.
+    axis : int, optional
+        The axis of the real transform.
+    output : ndarray, optional
+        If given, the result of shifting the input is placed in this array.
+
+    Returns
+    -------
+    fourier_shift : ndarray
+        The shifted input.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> import numpy.fft
+    >>> fig, (ax1, ax2) = plt.subplots(1, 2)
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> ascent = datasets.ascent()
+    >>> input_ = numpy.fft.fft2(ascent)
+    >>> result = ndimage.fourier_shift(input_, shift=200)
+    >>> result = numpy.fft.ifft2(result)
+    >>> ax1.imshow(ascent)
+    >>> ax2.imshow(result.real)  # the imaginary part is an artifact
+    >>> plt.show()
+    """
+    input = np.asarray(input)
+    output = _get_output_fourier_complex(output, input)
+    axis = normalize_axis_index(axis, input.ndim)
+    shifts = _ni_support._normalize_sequence(shift, input.ndim)
+    shifts = np.asarray(shifts, dtype=np.float64)
+    if not shifts.flags.contiguous:
+        shifts = shifts.copy()
+    _nd_image.fourier_shift(input, shifts, n, axis, output)
+    return output
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_interpolation.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_interpolation.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b8827d66ece8c6f76c58b1889f1450e9db3e924
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_interpolation.py
@@ -0,0 +1,1001 @@
+# Copyright (C) 2003-2005 Peter J. Verveer
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above
+#    copyright notice, this list of conditions and the following
+#    disclaimer in the documentation and/or other materials provided
+#    with the distribution.
+#
+# 3. The name of the author may not be used to endorse or promote
+#    products derived from this software without specific prior
+#    written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import itertools
+import warnings
+
+import numpy as np
+from scipy._lib._util import normalize_axis_index
+
+from scipy import special
+from . import _ni_support
+from . import _nd_image
+from ._ni_docstrings import docfiller
+
+
+__all__ = ['spline_filter1d', 'spline_filter', 'geometric_transform',
+           'map_coordinates', 'affine_transform', 'shift', 'zoom', 'rotate']
+
+
+@docfiller
+def spline_filter1d(input, order=3, axis=-1, output=np.float64,
+                    mode='mirror'):
+    """
+    Calculate a 1-D spline filter along the given axis.
+
+    The lines of the array along the given axis are filtered by a
+    spline filter. The order of the spline must be >= 2 and <= 5.
+
+    Parameters
+    ----------
+    %(input)s
+    order : int, optional
+        The order of the spline, default is 3.
+    axis : int, optional
+        The axis along which the spline filter is applied. Default is the last
+        axis.
+    output : ndarray or dtype, optional
+        The array in which to place the output, or the dtype of the returned
+        array. Default is ``numpy.float64``.
+    %(mode_interp_mirror)s
+
+    Returns
+    -------
+    spline_filter1d : ndarray
+        The filtered input.
+
+    See Also
+    --------
+    spline_filter : Multidimensional spline filter.
+
+    Notes
+    -----
+    All of the interpolation functions in `ndimage` do spline interpolation of
+    the input image. If using B-splines of `order > 1`, the input image
+    values have to be converted to B-spline coefficients first, which is
+    done by applying this 1-D filter sequentially along all
+    axes of the input. All functions that require B-spline coefficients
+    will automatically filter their inputs, a behavior controllable with
+    the `prefilter` keyword argument. For functions that accept a `mode`
+    parameter, the result will only be correct if it matches the `mode`
+    used when filtering.
+
+    For complex-valued `input`, this function processes the real and imaginary
+    components independently.
+
+    .. versionadded:: 1.6.0
+        Complex-valued support added.
+
+    Examples
+    --------
+    We can filter an image using 1-D spline along the given axis:
+
+    >>> from scipy.ndimage import spline_filter1d
+    >>> import numpy as np
+    >>> import matplotlib.pyplot as plt
+    >>> orig_img = np.eye(20)  # create an image
+    >>> orig_img[10, :] = 1.0
+    >>> sp_filter_axis_0 = spline_filter1d(orig_img, axis=0)
+    >>> sp_filter_axis_1 = spline_filter1d(orig_img, axis=1)
+    >>> f, ax = plt.subplots(1, 3, sharex=True)
+    >>> for ind, data in enumerate([[orig_img, "original image"],
+    ...             [sp_filter_axis_0, "spline filter (axis=0)"],
+    ...             [sp_filter_axis_1, "spline filter (axis=1)"]]):
+    ...     ax[ind].imshow(data[0], cmap='gray_r')
+    ...     ax[ind].set_title(data[1])
+    >>> plt.tight_layout()
+    >>> plt.show()
+
+    """
+    if order < 0 or order > 5:
+        raise RuntimeError('spline order not supported')
+    input = np.asarray(input)
+    complex_output = np.iscomplexobj(input)
+    output = _ni_support._get_output(output, input,
+                                     complex_output=complex_output)
+    if complex_output:
+        spline_filter1d(input.real, order, axis, output.real, mode)
+        spline_filter1d(input.imag, order, axis, output.imag, mode)
+        return output
+    if order in [0, 1]:
+        output[...] = np.array(input)
+    else:
+        mode = _ni_support._extend_mode_to_code(mode)
+        axis = normalize_axis_index(axis, input.ndim)
+        _nd_image.spline_filter1d(input, order, axis, output, mode)
+    return output
+
+@docfiller
+def spline_filter(input, order=3, output=np.float64, mode='mirror'):
+    """
+    Multidimensional spline filter.
+
+    Parameters
+    ----------
+    %(input)s
+    order : int, optional
+        The order of the spline, default is 3.
+    output : ndarray or dtype, optional
+        The array in which to place the output, or the dtype of the returned
+        array. Default is ``numpy.float64``.
+    %(mode_interp_mirror)s
+
+    Returns
+    -------
+    spline_filter : ndarray
+        Filtered array. Has the same shape as `input`.
+
+    See Also
+    --------
+    spline_filter1d : Calculate a 1-D spline filter along the given axis.
+
+    Notes
+    -----
+    The multidimensional filter is implemented as a sequence of
+    1-D spline filters. The intermediate arrays are stored
+    in the same data type as the output. Therefore, for output types
+    with a limited precision, the results may be imprecise because
+    intermediate results may be stored with insufficient precision.
+
+    For complex-valued `input`, this function processes the real and imaginary
+    components independently.
+
+    .. versionadded:: 1.6.0
+        Complex-valued support added.
+
+    Examples
+    --------
+    We can filter an image using multidimentional splines:
+
+    >>> from scipy.ndimage import spline_filter
+    >>> import numpy as np
+    >>> import matplotlib.pyplot as plt
+    >>> orig_img = np.eye(20)  # create an image
+    >>> orig_img[10, :] = 1.0
+    >>> sp_filter = spline_filter(orig_img, order=3)
+    >>> f, ax = plt.subplots(1, 2, sharex=True)
+    >>> for ind, data in enumerate([[orig_img, "original image"],
+    ...                             [sp_filter, "spline filter"]]):
+    ...     ax[ind].imshow(data[0], cmap='gray_r')
+    ...     ax[ind].set_title(data[1])
+    >>> plt.tight_layout()
+    >>> plt.show()
+
+    """
+    if order < 2 or order > 5:
+        raise RuntimeError('spline order not supported')
+    input = np.asarray(input)
+    complex_output = np.iscomplexobj(input)
+    output = _ni_support._get_output(output, input,
+                                     complex_output=complex_output)
+    if complex_output:
+        spline_filter(input.real, order, output.real, mode)
+        spline_filter(input.imag, order, output.imag, mode)
+        return output
+    if order not in [0, 1] and input.ndim > 0:
+        for axis in range(input.ndim):
+            spline_filter1d(input, order, axis, output=output, mode=mode)
+            input = output
+    else:
+        output[...] = input[...]
+    return output
+
+
+def _prepad_for_spline_filter(input, mode, cval):
+    if mode in ['nearest', 'grid-constant']:
+        npad = 12
+        if mode == 'grid-constant':
+            padded = np.pad(input, npad, mode='constant',
+                               constant_values=cval)
+        elif mode == 'nearest':
+            padded = np.pad(input, npad, mode='edge')
+    else:
+        # other modes have exact boundary conditions implemented so
+        # no prepadding is needed
+        npad = 0
+        padded = input
+    return padded, npad
+
+
+@docfiller
+def geometric_transform(input, mapping, output_shape=None,
+                        output=None, order=3,
+                        mode='constant', cval=0.0, prefilter=True,
+                        extra_arguments=(), extra_keywords={}):
+    """
+    Apply an arbitrary geometric transform.
+
+    The given mapping function is used to find, for each point in the
+    output, the corresponding coordinates in the input. The value of the
+    input at those coordinates is determined by spline interpolation of
+    the requested order.
+
+    Parameters
+    ----------
+    %(input)s
+    mapping : {callable, scipy.LowLevelCallable}
+        A callable object that accepts a tuple of length equal to the output
+        array rank, and returns the corresponding input coordinates as a tuple
+        of length equal to the input array rank.
+    output_shape : tuple of ints, optional
+        Shape tuple.
+    %(output)s
+    order : int, optional
+        The order of the spline interpolation, default is 3.
+        The order has to be in the range 0-5.
+    %(mode_interp_constant)s
+    %(cval)s
+    %(prefilter)s
+    extra_arguments : tuple, optional
+        Extra arguments passed to `mapping`.
+    extra_keywords : dict, optional
+        Extra keywords passed to `mapping`.
+
+    Returns
+    -------
+    output : ndarray
+        The filtered input.
+
+    See Also
+    --------
+    map_coordinates, affine_transform, spline_filter1d
+
+
+    Notes
+    -----
+    This function also accepts low-level callback functions with one
+    the following signatures and wrapped in `scipy.LowLevelCallable`:
+
+    .. code:: c
+
+       int mapping(npy_intp *output_coordinates, double *input_coordinates,
+                   int output_rank, int input_rank, void *user_data)
+       int mapping(intptr_t *output_coordinates, double *input_coordinates,
+                   int output_rank, int input_rank, void *user_data)
+
+    The calling function iterates over the elements of the output array,
+    calling the callback function at each element. The coordinates of the
+    current output element are passed through ``output_coordinates``. The
+    callback function must return the coordinates at which the input must
+    be interpolated in ``input_coordinates``. The rank of the input and
+    output arrays are given by ``input_rank`` and ``output_rank``
+    respectively. ``user_data`` is the data pointer provided
+    to `scipy.LowLevelCallable` as-is.
+
+    The callback function must return an integer error status that is zero
+    if something went wrong and one otherwise. If an error occurs, you should
+    normally set the Python error status with an informative message
+    before returning, otherwise a default error message is set by the
+    calling function.
+
+    In addition, some other low-level function pointer specifications
+    are accepted, but these are for backward compatibility only and should
+    not be used in new code.
+
+    For complex-valued `input`, this function transforms the real and imaginary
+    components independently.
+
+    .. versionadded:: 1.6.0
+        Complex-valued support added.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.ndimage import geometric_transform
+    >>> a = np.arange(12.).reshape((4, 3))
+    >>> def shift_func(output_coords):
+    ...     return (output_coords[0] - 0.5, output_coords[1] - 0.5)
+    ...
+    >>> geometric_transform(a, shift_func)
+    array([[ 0.   ,  0.   ,  0.   ],
+           [ 0.   ,  1.362,  2.738],
+           [ 0.   ,  4.812,  6.187],
+           [ 0.   ,  8.263,  9.637]])
+
+    >>> b = [1, 2, 3, 4, 5]
+    >>> def shift_func(output_coords):
+    ...     return (output_coords[0] - 3,)
+    ...
+    >>> geometric_transform(b, shift_func, mode='constant')
+    array([0, 0, 0, 1, 2])
+    >>> geometric_transform(b, shift_func, mode='nearest')
+    array([1, 1, 1, 1, 2])
+    >>> geometric_transform(b, shift_func, mode='reflect')
+    array([3, 2, 1, 1, 2])
+    >>> geometric_transform(b, shift_func, mode='wrap')
+    array([2, 3, 4, 1, 2])
+
+    """
+    if order < 0 or order > 5:
+        raise RuntimeError('spline order not supported')
+    input = np.asarray(input)
+    if output_shape is None:
+        output_shape = input.shape
+    if input.ndim < 1 or len(output_shape) < 1:
+        raise RuntimeError('input and output rank must be > 0')
+    complex_output = np.iscomplexobj(input)
+    output = _ni_support._get_output(output, input, shape=output_shape,
+                                     complex_output=complex_output)
+    if complex_output:
+        kwargs = dict(order=order, mode=mode, prefilter=prefilter,
+                      output_shape=output_shape,
+                      extra_arguments=extra_arguments,
+                      extra_keywords=extra_keywords)
+        geometric_transform(input.real, mapping, output=output.real,
+                            cval=np.real(cval), **kwargs)
+        geometric_transform(input.imag, mapping, output=output.imag,
+                            cval=np.imag(cval), **kwargs)
+        return output
+
+    if prefilter and order > 1:
+        padded, npad = _prepad_for_spline_filter(input, mode, cval)
+        filtered = spline_filter(padded, order, output=np.float64,
+                                 mode=mode)
+    else:
+        npad = 0
+        filtered = input
+    mode = _ni_support._extend_mode_to_code(mode)
+    _nd_image.geometric_transform(filtered, mapping, None, None, None, output,
+                                  order, mode, cval, npad, extra_arguments,
+                                  extra_keywords)
+    return output
+
+
+@docfiller
+def map_coordinates(input, coordinates, output=None, order=3,
+                    mode='constant', cval=0.0, prefilter=True):
+    """
+    Map the input array to new coordinates by interpolation.
+
+    The array of coordinates is used to find, for each point in the output,
+    the corresponding coordinates in the input. The value of the input at
+    those coordinates is determined by spline interpolation of the
+    requested order.
+
+    The shape of the output is derived from that of the coordinate
+    array by dropping the first axis. The values of the array along
+    the first axis are the coordinates in the input array at which the
+    output value is found.
+
+    Parameters
+    ----------
+    %(input)s
+    coordinates : array_like
+        The coordinates at which `input` is evaluated.
+    %(output)s
+    order : int, optional
+        The order of the spline interpolation, default is 3.
+        The order has to be in the range 0-5.
+    %(mode_interp_constant)s
+    %(cval)s
+    %(prefilter)s
+
+    Returns
+    -------
+    map_coordinates : ndarray
+        The result of transforming the input. The shape of the output is
+        derived from that of `coordinates` by dropping the first axis.
+
+    See Also
+    --------
+    spline_filter, geometric_transform, scipy.interpolate
+
+    Notes
+    -----
+    For complex-valued `input`, this function maps the real and imaginary
+    components independently.
+
+    .. versionadded:: 1.6.0
+        Complex-valued support added.
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.arange(12.).reshape((4, 3))
+    >>> a
+    array([[  0.,   1.,   2.],
+           [  3.,   4.,   5.],
+           [  6.,   7.,   8.],
+           [  9.,  10.,  11.]])
+    >>> ndimage.map_coordinates(a, [[0.5, 2], [0.5, 1]], order=1)
+    array([ 2.,  7.])
+
+    Above, the interpolated value of a[0.5, 0.5] gives output[0], while
+    a[2, 1] is output[1].
+
+    >>> inds = np.array([[0.5, 2], [0.5, 4]])
+    >>> ndimage.map_coordinates(a, inds, order=1, cval=-33.3)
+    array([  2. , -33.3])
+    >>> ndimage.map_coordinates(a, inds, order=1, mode='nearest')
+    array([ 2.,  8.])
+    >>> ndimage.map_coordinates(a, inds, order=1, cval=0, output=bool)
+    array([ True, False], dtype=bool)
+
+    """
+    if order < 0 or order > 5:
+        raise RuntimeError('spline order not supported')
+    input = np.asarray(input)
+    coordinates = np.asarray(coordinates)
+    if np.iscomplexobj(coordinates):
+        raise TypeError('Complex type not supported')
+    output_shape = coordinates.shape[1:]
+    if input.ndim < 1 or len(output_shape) < 1:
+        raise RuntimeError('input and output rank must be > 0')
+    if coordinates.shape[0] != input.ndim:
+        raise RuntimeError('invalid shape for coordinate array')
+    complex_output = np.iscomplexobj(input)
+    output = _ni_support._get_output(output, input, shape=output_shape,
+                                     complex_output=complex_output)
+    if complex_output:
+        kwargs = dict(order=order, mode=mode, prefilter=prefilter)
+        map_coordinates(input.real, coordinates, output=output.real,
+                        cval=np.real(cval), **kwargs)
+        map_coordinates(input.imag, coordinates, output=output.imag,
+                        cval=np.imag(cval), **kwargs)
+        return output
+    if prefilter and order > 1:
+        padded, npad = _prepad_for_spline_filter(input, mode, cval)
+        filtered = spline_filter(padded, order, output=np.float64, mode=mode)
+    else:
+        npad = 0
+        filtered = input
+    mode = _ni_support._extend_mode_to_code(mode)
+    _nd_image.geometric_transform(filtered, None, coordinates, None, None,
+                                  output, order, mode, cval, npad, None, None)
+    return output
+
+
+@docfiller
+def affine_transform(input, matrix, offset=0.0, output_shape=None,
+                     output=None, order=3,
+                     mode='constant', cval=0.0, prefilter=True):
+    """
+    Apply an affine transformation.
+
+    Given an output image pixel index vector ``o``, the pixel value
+    is determined from the input image at position
+    ``np.dot(matrix, o) + offset``.
+
+    This does 'pull' (or 'backward') resampling, transforming the output space
+    to the input to locate data. Affine transformations are often described in
+    the 'push' (or 'forward') direction, transforming input to output. If you
+    have a matrix for the 'push' transformation, use its inverse
+    (:func:`numpy.linalg.inv`) in this function.
+
+    Parameters
+    ----------
+    %(input)s
+    matrix : ndarray
+        The inverse coordinate transformation matrix, mapping output
+        coordinates to input coordinates. If ``ndim`` is the number of
+        dimensions of ``input``, the given matrix must have one of the
+        following shapes:
+
+            - ``(ndim, ndim)``: the linear transformation matrix for each
+              output coordinate.
+            - ``(ndim,)``: assume that the 2-D transformation matrix is
+              diagonal, with the diagonal specified by the given value. A more
+              efficient algorithm is then used that exploits the separability
+              of the problem.
+            - ``(ndim + 1, ndim + 1)``: assume that the transformation is
+              specified using homogeneous coordinates [1]_. In this case, any
+              value passed to ``offset`` is ignored.
+            - ``(ndim, ndim + 1)``: as above, but the bottom row of a
+              homogeneous transformation matrix is always ``[0, 0, ..., 1]``,
+              and may be omitted.
+
+    offset : float or sequence, optional
+        The offset into the array where the transform is applied. If a float,
+        `offset` is the same for each axis. If a sequence, `offset` should
+        contain one value for each axis.
+    output_shape : tuple of ints, optional
+        Shape tuple.
+    %(output)s
+    order : int, optional
+        The order of the spline interpolation, default is 3.
+        The order has to be in the range 0-5.
+    %(mode_interp_constant)s
+    %(cval)s
+    %(prefilter)s
+
+    Returns
+    -------
+    affine_transform : ndarray
+        The transformed input.
+
+    Notes
+    -----
+    The given matrix and offset are used to find for each point in the
+    output the corresponding coordinates in the input by an affine
+    transformation. The value of the input at those coordinates is
+    determined by spline interpolation of the requested order. Points
+    outside the boundaries of the input are filled according to the given
+    mode.
+
+    .. versionchanged:: 0.18.0
+        Previously, the exact interpretation of the affine transformation
+        depended on whether the matrix was supplied as a 1-D or a
+        2-D array. If a 1-D array was supplied
+        to the matrix parameter, the output pixel value at index ``o``
+        was determined from the input image at position
+        ``matrix * (o + offset)``.
+
+    For complex-valued `input`, this function transforms the real and imaginary
+    components independently.
+
+    .. versionadded:: 1.6.0
+        Complex-valued support added.
+
+    References
+    ----------
+    .. [1] https://en.wikipedia.org/wiki/Homogeneous_coordinates
+    """
+    if order < 0 or order > 5:
+        raise RuntimeError('spline order not supported')
+    input = np.asarray(input)
+    if output_shape is None:
+        if isinstance(output, np.ndarray):
+            output_shape = output.shape
+        else:
+            output_shape = input.shape
+    if input.ndim < 1 or len(output_shape) < 1:
+        raise RuntimeError('input and output rank must be > 0')
+    complex_output = np.iscomplexobj(input)
+    output = _ni_support._get_output(output, input, shape=output_shape,
+                                     complex_output=complex_output)
+    if complex_output:
+        kwargs = dict(offset=offset, output_shape=output_shape, order=order,
+                      mode=mode, prefilter=prefilter)
+        affine_transform(input.real, matrix, output=output.real,
+                         cval=np.real(cval), **kwargs)
+        affine_transform(input.imag, matrix, output=output.imag,
+                         cval=np.imag(cval), **kwargs)
+        return output
+    if prefilter and order > 1:
+        padded, npad = _prepad_for_spline_filter(input, mode, cval)
+        filtered = spline_filter(padded, order, output=np.float64, mode=mode)
+    else:
+        npad = 0
+        filtered = input
+    mode = _ni_support._extend_mode_to_code(mode)
+    matrix = np.asarray(matrix, dtype=np.float64)
+    if matrix.ndim not in [1, 2] or matrix.shape[0] < 1:
+        raise RuntimeError('no proper affine matrix provided')
+    if (matrix.ndim == 2 and matrix.shape[1] == input.ndim + 1 and
+            (matrix.shape[0] in [input.ndim, input.ndim + 1])):
+        if matrix.shape[0] == input.ndim + 1:
+            exptd = [0] * input.ndim + [1]
+            if not np.all(matrix[input.ndim] == exptd):
+                msg = (f'Expected homogeneous transformation matrix with '
+                       f'shape {matrix.shape} for image shape {input.shape}, '
+                       f'but bottom row was not equal to {exptd}')
+                raise ValueError(msg)
+        # assume input is homogeneous coordinate transformation matrix
+        offset = matrix[:input.ndim, input.ndim]
+        matrix = matrix[:input.ndim, :input.ndim]
+    if matrix.shape[0] != input.ndim:
+        raise RuntimeError('affine matrix has wrong number of rows')
+    if matrix.ndim == 2 and matrix.shape[1] != output.ndim:
+        raise RuntimeError('affine matrix has wrong number of columns')
+    if not matrix.flags.contiguous:
+        matrix = matrix.copy()
+    offset = _ni_support._normalize_sequence(offset, input.ndim)
+    offset = np.asarray(offset, dtype=np.float64)
+    if offset.ndim != 1 or offset.shape[0] < 1:
+        raise RuntimeError('no proper offset provided')
+    if not offset.flags.contiguous:
+        offset = offset.copy()
+    if matrix.ndim == 1:
+        warnings.warn(
+            "The behavior of affine_transform with a 1-D "
+            "array supplied for the matrix parameter has changed in "
+            "SciPy 0.18.0.",
+            stacklevel=2
+        )
+        _nd_image.zoom_shift(filtered, matrix, offset/matrix, output, order,
+                             mode, cval, npad, False)
+    else:
+        _nd_image.geometric_transform(filtered, None, None, matrix, offset,
+                                      output, order, mode, cval, npad, None,
+                                      None)
+    return output
+
+
+@docfiller
+def shift(input, shift, output=None, order=3, mode='constant', cval=0.0,
+          prefilter=True):
+    """
+    Shift an array.
+
+    The array is shifted using spline interpolation of the requested order.
+    Points outside the boundaries of the input are filled according to the
+    given mode.
+
+    Parameters
+    ----------
+    %(input)s
+    shift : float or sequence
+        The shift along the axes. If a float, `shift` is the same for each
+        axis. If a sequence, `shift` should contain one value for each axis.
+    %(output)s
+    order : int, optional
+        The order of the spline interpolation, default is 3.
+        The order has to be in the range 0-5.
+    %(mode_interp_constant)s
+    %(cval)s
+    %(prefilter)s
+
+    Returns
+    -------
+    shift : ndarray
+        The shifted input.
+
+    See Also
+    --------
+    affine_transform : Affine transformations
+
+    Notes
+    -----
+    For complex-valued `input`, this function shifts the real and imaginary
+    components independently.
+
+    .. versionadded:: 1.6.0
+        Complex-valued support added.
+
+    Examples
+    --------
+    Import the necessary modules and an exemplary image.
+
+    >>> from scipy.ndimage import shift
+    >>> import matplotlib.pyplot as plt
+    >>> from scipy import datasets
+    >>> image = datasets.ascent()
+
+    Shift the image vertically by 20 pixels.
+
+    >>> image_shifted_vertically = shift(image, (20, 0))
+
+    Shift the image vertically by -200 pixels and horizontally by 100 pixels.
+
+    >>> image_shifted_both_directions = shift(image, (-200, 100))
+
+    Plot the original and the shifted images.
+
+    >>> fig, axes = plt.subplots(3, 1, figsize=(4, 12))
+    >>> plt.gray()  # show the filtered result in grayscale
+    >>> top, middle, bottom = axes
+    >>> for ax in axes:
+    ...     ax.set_axis_off()  # remove coordinate system
+    >>> top.imshow(image)
+    >>> top.set_title("Original image")
+    >>> middle.imshow(image_shifted_vertically)
+    >>> middle.set_title("Vertically shifted image")
+    >>> bottom.imshow(image_shifted_both_directions)
+    >>> bottom.set_title("Image shifted in both directions")
+    >>> fig.tight_layout()
+    """
+    if order < 0 or order > 5:
+        raise RuntimeError('spline order not supported')
+    input = np.asarray(input)
+    if input.ndim < 1:
+        raise RuntimeError('input and output rank must be > 0')
+    complex_output = np.iscomplexobj(input)
+    output = _ni_support._get_output(output, input, complex_output=complex_output)
+    if complex_output:
+        # import under different name to avoid confusion with shift parameter
+        from scipy.ndimage._interpolation import shift as _shift
+
+        kwargs = dict(order=order, mode=mode, prefilter=prefilter)
+        _shift(input.real, shift, output=output.real, cval=np.real(cval), **kwargs)
+        _shift(input.imag, shift, output=output.imag, cval=np.imag(cval), **kwargs)
+        return output
+    if prefilter and order > 1:
+        padded, npad = _prepad_for_spline_filter(input, mode, cval)
+        filtered = spline_filter(padded, order, output=np.float64, mode=mode)
+    else:
+        npad = 0
+        filtered = input
+    mode = _ni_support._extend_mode_to_code(mode)
+    shift = _ni_support._normalize_sequence(shift, input.ndim)
+    shift = [-ii for ii in shift]
+    shift = np.asarray(shift, dtype=np.float64)
+    if not shift.flags.contiguous:
+        shift = shift.copy()
+    _nd_image.zoom_shift(filtered, None, shift, output, order, mode, cval,
+                         npad, False)
+    return output
+
+
+@docfiller
+def zoom(input, zoom, output=None, order=3, mode='constant', cval=0.0,
+         prefilter=True, *, grid_mode=False):
+    """
+    Zoom an array.
+
+    The array is zoomed using spline interpolation of the requested order.
+
+    Parameters
+    ----------
+    %(input)s
+    zoom : float or sequence
+        The zoom factor along the axes. If a float, `zoom` is the same for each
+        axis. If a sequence, `zoom` should contain one value for each axis.
+    %(output)s
+    order : int, optional
+        The order of the spline interpolation, default is 3.
+        The order has to be in the range 0-5.
+    %(mode_interp_constant)s
+    %(cval)s
+    %(prefilter)s
+    grid_mode : bool, optional
+        If False, the distance from the pixel centers is zoomed. Otherwise, the
+        distance including the full pixel extent is used. For example, a 1d
+        signal of length 5 is considered to have length 4 when `grid_mode` is
+        False, but length 5 when `grid_mode` is True. See the following
+        visual illustration:
+
+        .. code-block:: text
+
+                | pixel 1 | pixel 2 | pixel 3 | pixel 4 | pixel 5 |
+                     |<-------------------------------------->|
+                                        vs.
+                |<----------------------------------------------->|
+
+        The starting point of the arrow in the diagram above corresponds to
+        coordinate location 0 in each mode.
+
+    Returns
+    -------
+    zoom : ndarray
+        The zoomed input.
+
+    Notes
+    -----
+    For complex-valued `input`, this function zooms the real and imaginary
+    components independently.
+
+    .. versionadded:: 1.6.0
+        Complex-valued support added.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+
+    >>> fig = plt.figure()
+    >>> ax1 = fig.add_subplot(121)  # left side
+    >>> ax2 = fig.add_subplot(122)  # right side
+    >>> ascent = datasets.ascent()
+    >>> result = ndimage.zoom(ascent, 3.0)
+    >>> ax1.imshow(ascent, vmin=0, vmax=255)
+    >>> ax2.imshow(result, vmin=0, vmax=255)
+    >>> plt.show()
+
+    >>> print(ascent.shape)
+    (512, 512)
+
+    >>> print(result.shape)
+    (1536, 1536)
+    """
+    if order < 0 or order > 5:
+        raise RuntimeError('spline order not supported')
+    input = np.asarray(input)
+    if input.ndim < 1:
+        raise RuntimeError('input and output rank must be > 0')
+    zoom = _ni_support._normalize_sequence(zoom, input.ndim)
+    output_shape = tuple(
+            [int(round(ii * jj)) for ii, jj in zip(input.shape, zoom)])
+    complex_output = np.iscomplexobj(input)
+    output = _ni_support._get_output(output, input, shape=output_shape,
+                                     complex_output=complex_output)
+    if complex_output:
+        # import under different name to avoid confusion with zoom parameter
+        from scipy.ndimage._interpolation import zoom as _zoom
+
+        kwargs = dict(order=order, mode=mode, prefilter=prefilter)
+        _zoom(input.real, zoom, output=output.real, cval=np.real(cval), **kwargs)
+        _zoom(input.imag, zoom, output=output.imag, cval=np.imag(cval), **kwargs)
+        return output
+    if prefilter and order > 1:
+        padded, npad = _prepad_for_spline_filter(input, mode, cval)
+        filtered = spline_filter(padded, order, output=np.float64, mode=mode)
+    else:
+        npad = 0
+        filtered = input
+    if grid_mode:
+        # warn about modes that may have surprising behavior
+        suggest_mode = None
+        if mode == 'constant':
+            suggest_mode = 'grid-constant'
+        elif mode == 'wrap':
+            suggest_mode = 'grid-wrap'
+        if suggest_mode is not None:
+            warnings.warn(
+                (f"It is recommended to use mode = {suggest_mode} instead of {mode} "
+                 f"when grid_mode is True."),
+                stacklevel=2
+            )
+    mode = _ni_support._extend_mode_to_code(mode)
+
+    zoom_div = np.array(output_shape)
+    zoom_nominator = np.array(input.shape)
+    if not grid_mode:
+        zoom_div -= 1
+        zoom_nominator -= 1
+
+    # Zooming to infinite values is unpredictable, so just choose
+    # zoom factor 1 instead
+    zoom = np.divide(zoom_nominator, zoom_div,
+                     out=np.ones_like(input.shape, dtype=np.float64),
+                     where=zoom_div != 0)
+    zoom = np.ascontiguousarray(zoom)
+    _nd_image.zoom_shift(filtered, zoom, None, output, order, mode, cval, npad,
+                         grid_mode)
+    return output
+
+
+@docfiller
+def rotate(input, angle, axes=(1, 0), reshape=True, output=None, order=3,
+           mode='constant', cval=0.0, prefilter=True):
+    """
+    Rotate an array.
+
+    The array is rotated in the plane defined by the two axes given by the
+    `axes` parameter using spline interpolation of the requested order.
+
+    Parameters
+    ----------
+    %(input)s
+    angle : float
+        The rotation angle in degrees.
+    axes : tuple of 2 ints, optional
+        The two axes that define the plane of rotation. Default is the first
+        two axes.
+    reshape : bool, optional
+        If `reshape` is true, the output shape is adapted so that the input
+        array is contained completely in the output. Default is True.
+    %(output)s
+    order : int, optional
+        The order of the spline interpolation, default is 3.
+        The order has to be in the range 0-5.
+    %(mode_interp_constant)s
+    %(cval)s
+    %(prefilter)s
+
+    Returns
+    -------
+    rotate : ndarray
+        The rotated input.
+
+    Notes
+    -----
+    For complex-valued `input`, this function rotates the real and imaginary
+    components independently.
+
+    .. versionadded:: 1.6.0
+        Complex-valued support added.
+
+    Examples
+    --------
+    >>> from scipy import ndimage, datasets
+    >>> import matplotlib.pyplot as plt
+    >>> fig = plt.figure(figsize=(10, 3))
+    >>> ax1, ax2, ax3 = fig.subplots(1, 3)
+    >>> img = datasets.ascent()
+    >>> img_45 = ndimage.rotate(img, 45, reshape=False)
+    >>> full_img_45 = ndimage.rotate(img, 45, reshape=True)
+    >>> ax1.imshow(img, cmap='gray')
+    >>> ax1.set_axis_off()
+    >>> ax2.imshow(img_45, cmap='gray')
+    >>> ax2.set_axis_off()
+    >>> ax3.imshow(full_img_45, cmap='gray')
+    >>> ax3.set_axis_off()
+    >>> fig.set_layout_engine('tight')
+    >>> plt.show()
+    >>> print(img.shape)
+    (512, 512)
+    >>> print(img_45.shape)
+    (512, 512)
+    >>> print(full_img_45.shape)
+    (724, 724)
+
+    """
+    input_arr = np.asarray(input)
+    ndim = input_arr.ndim
+
+    if ndim < 2:
+        raise ValueError('input array should be at least 2D')
+
+    axes = list(axes)
+
+    if len(axes) != 2:
+        raise ValueError('axes should contain exactly two values')
+
+    if not all([float(ax).is_integer() for ax in axes]):
+        raise ValueError('axes should contain only integer values')
+
+    if axes[0] < 0:
+        axes[0] += ndim
+    if axes[1] < 0:
+        axes[1] += ndim
+    if axes[0] < 0 or axes[1] < 0 or axes[0] >= ndim or axes[1] >= ndim:
+        raise ValueError('invalid rotation plane specified')
+
+    axes.sort()
+
+    c, s = special.cosdg(angle), special.sindg(angle)
+
+    rot_matrix = np.array([[c, s],
+                           [-s, c]])
+
+    img_shape = np.asarray(input_arr.shape)
+    in_plane_shape = img_shape[axes]
+    if reshape:
+        # Compute transformed input bounds
+        iy, ix = in_plane_shape
+        out_bounds = rot_matrix @ [[0, 0, iy, iy],
+                                   [0, ix, 0, ix]]
+        # Compute the shape of the transformed input plane
+        out_plane_shape = (np.ptp(out_bounds, axis=1) + 0.5).astype(int)
+    else:
+        out_plane_shape = img_shape[axes]
+
+    out_center = rot_matrix @ ((out_plane_shape - 1) / 2)
+    in_center = (in_plane_shape - 1) / 2
+    offset = in_center - out_center
+
+    output_shape = img_shape
+    output_shape[axes] = out_plane_shape
+    output_shape = tuple(output_shape)
+
+    complex_output = np.iscomplexobj(input_arr)
+    output = _ni_support._get_output(output, input_arr, shape=output_shape,
+                                     complex_output=complex_output)
+
+    if ndim <= 2:
+        affine_transform(input_arr, rot_matrix, offset, output_shape, output,
+                         order, mode, cval, prefilter)
+    else:
+        # If ndim > 2, the rotation is applied over all the planes
+        # parallel to axes
+        planes_coord = itertools.product(
+            *[[slice(None)] if ax in axes else range(img_shape[ax])
+              for ax in range(ndim)])
+
+        out_plane_shape = tuple(out_plane_shape)
+
+        for coordinates in planes_coord:
+            ia = input_arr[coordinates]
+            oa = output[coordinates]
+            affine_transform(ia, rot_matrix, offset, out_plane_shape,
+                             oa, order, mode, cval, prefilter)
+
+    return output
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_measurements.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_measurements.py
new file mode 100644
index 0000000000000000000000000000000000000000..bcd83df42be3708231870cf5eff977a6388cc3a1
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_measurements.py
@@ -0,0 +1,1680 @@
+# Copyright (C) 2003-2005 Peter J. Verveer
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above
+#    copyright notice, this list of conditions and the following
+#    disclaimer in the documentation and/or other materials provided
+#    with the distribution.
+#
+# 3. The name of the author may not be used to endorse or promote
+#    products derived from this software without specific prior
+#    written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import numpy as np
+from . import _ni_support
+from . import _ni_label
+from . import _nd_image
+from . import _morphology
+
+__all__ = ['label', 'find_objects', 'labeled_comprehension', 'sum', 'mean',
+           'variance', 'standard_deviation', 'minimum', 'maximum', 'median',
+           'minimum_position', 'maximum_position', 'extrema', 'center_of_mass',
+           'histogram', 'watershed_ift', 'sum_labels', 'value_indices']
+
+
+def label(input, structure=None, output=None):
+    """
+    Label features in an array.
+
+    Parameters
+    ----------
+    input : array_like
+        An array-like object to be labeled. Any non-zero values in `input` are
+        counted as features and zero values are considered the background.
+    structure : array_like, optional
+        A structuring element that defines feature connections.
+        `structure` must be centrosymmetric
+        (see Notes).
+        If no structuring element is provided,
+        one is automatically generated with a squared connectivity equal to
+        one.  That is, for a 2-D `input` array, the default structuring element
+        is::
+
+            [[0,1,0],
+             [1,1,1],
+             [0,1,0]]
+
+    output : (None, data-type, array_like), optional
+        If `output` is a data type, it specifies the type of the resulting
+        labeled feature array.
+        If `output` is an array-like object, then `output` will be updated
+        with the labeled features from this function.  This function can
+        operate in-place, by passing output=input.
+        Note that the output must be able to store the largest label, or this
+        function will raise an Exception.
+
+    Returns
+    -------
+    label : ndarray or int
+        An integer ndarray where each unique feature in `input` has a unique
+        label in the returned array.
+    num_features : int
+        How many objects were found.
+
+        If `output` is None, this function returns a tuple of
+        (`labeled_array`, `num_features`).
+
+        If `output` is a ndarray, then it will be updated with values in
+        `labeled_array` and only `num_features` will be returned by this
+        function.
+
+    See Also
+    --------
+    find_objects : generate a list of slices for the labeled features (or
+                   objects); useful for finding features' position or
+                   dimensions
+
+    Notes
+    -----
+    A centrosymmetric matrix is a matrix that is symmetric about the center.
+    See [1]_ for more information.
+
+    The `structure` matrix must be centrosymmetric to ensure
+    two-way connections.
+    For instance, if the `structure` matrix is not centrosymmetric
+    and is defined as::
+
+        [[0,1,0],
+         [1,1,0],
+         [0,0,0]]
+
+    and the `input` is::
+
+        [[1,2],
+         [0,3]]
+
+    then the structure matrix would indicate the
+    entry 2 in the input is connected to 1,
+    but 1 is not connected to 2.
+
+    References
+    ----------
+    .. [1] James R. Weaver, "Centrosymmetric (cross-symmetric)
+       matrices, their basic properties, eigenvalues, and
+       eigenvectors." The American Mathematical Monthly 92.10
+       (1985): 711-717.
+
+    Examples
+    --------
+    Create an image with some features, then label it using the default
+    (cross-shaped) structuring element:
+
+    >>> from scipy.ndimage import label, generate_binary_structure
+    >>> import numpy as np
+    >>> a = np.array([[0,0,1,1,0,0],
+    ...               [0,0,0,1,0,0],
+    ...               [1,1,0,0,1,0],
+    ...               [0,0,0,1,0,0]])
+    >>> labeled_array, num_features = label(a)
+
+    Each of the 4 features are labeled with a different integer:
+
+    >>> num_features
+    4
+    >>> labeled_array
+    array([[0, 0, 1, 1, 0, 0],
+           [0, 0, 0, 1, 0, 0],
+           [2, 2, 0, 0, 3, 0],
+           [0, 0, 0, 4, 0, 0]])
+
+    Generate a structuring element that will consider features connected even
+    if they touch diagonally:
+
+    >>> s = generate_binary_structure(2,2)
+
+    or,
+
+    >>> s = [[1,1,1],
+    ...      [1,1,1],
+    ...      [1,1,1]]
+
+    Label the image using the new structuring element:
+
+    >>> labeled_array, num_features = label(a, structure=s)
+
+    Show the 2 labeled features (note that features 1, 3, and 4 from above are
+    now considered a single feature):
+
+    >>> num_features
+    2
+    >>> labeled_array
+    array([[0, 0, 1, 1, 0, 0],
+           [0, 0, 0, 1, 0, 0],
+           [2, 2, 0, 0, 1, 0],
+           [0, 0, 0, 1, 0, 0]])
+
+    """
+    input = np.asarray(input)
+    if np.iscomplexobj(input):
+        raise TypeError('Complex type not supported')
+    if structure is None:
+        structure = _morphology.generate_binary_structure(input.ndim, 1)
+    structure = np.asarray(structure, dtype=bool)
+    if structure.ndim != input.ndim:
+        raise RuntimeError('structure and input must have equal rank')
+    for ii in structure.shape:
+        if ii != 3:
+            raise ValueError('structure dimensions must be equal to 3')
+
+    # Use 32 bits if it's large enough for this image.
+    # _ni_label.label() needs two entries for background and
+    # foreground tracking
+    need_64bits = input.size >= (2**31 - 2)
+
+    if isinstance(output, np.ndarray):
+        if output.shape != input.shape:
+            raise ValueError("output shape not correct")
+        caller_provided_output = True
+    else:
+        caller_provided_output = False
+        if output is None:
+            output = np.empty(input.shape, np.intp if need_64bits else np.int32)
+        else:
+            output = np.empty(input.shape, output)
+
+    # handle scalars, 0-D arrays
+    if input.ndim == 0 or input.size == 0:
+        if input.ndim == 0:
+            # scalar
+            maxlabel = 1 if (input != 0) else 0
+            output[...] = maxlabel
+        else:
+            # 0-D
+            maxlabel = 0
+        if caller_provided_output:
+            return maxlabel
+        else:
+            return output, maxlabel
+
+    try:
+        max_label = _ni_label._label(input, structure, output)
+    except _ni_label.NeedMoreBits as e:
+        # Make another attempt with enough bits, then try to cast to the
+        # new type.
+        tmp_output = np.empty(input.shape, np.intp if need_64bits else np.int32)
+        max_label = _ni_label._label(input, structure, tmp_output)
+        output[...] = tmp_output[...]
+        if not np.all(output == tmp_output):
+            # refuse to return bad results
+            raise RuntimeError(
+                "insufficient bit-depth in requested output type"
+            ) from e
+
+    if caller_provided_output:
+        # result was written in-place
+        return max_label
+    else:
+        return output, max_label
+
+
+def find_objects(input, max_label=0):
+    """
+    Find objects in a labeled array.
+
+    Parameters
+    ----------
+    input : ndarray of ints
+        Array containing objects defined by different labels. Labels with
+        value 0 are ignored.
+    max_label : int, optional
+        Maximum label to be searched for in `input`. If max_label is not
+        given, the positions of all objects are returned.
+
+    Returns
+    -------
+    object_slices : list of tuples
+        A list of tuples, with each tuple containing N slices (with N the
+        dimension of the input array). Slices correspond to the minimal
+        parallelepiped that contains the object. If a number is missing,
+        None is returned instead of a slice. The label ``l`` corresponds to
+        the index ``l-1`` in the returned list.
+
+    See Also
+    --------
+    label, center_of_mass
+
+    Notes
+    -----
+    This function is very useful for isolating a volume of interest inside
+    a 3-D array, that cannot be "seen through".
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.zeros((6,6), dtype=int)
+    >>> a[2:4, 2:4] = 1
+    >>> a[4, 4] = 1
+    >>> a[:2, :3] = 2
+    >>> a[0, 5] = 3
+    >>> a
+    array([[2, 2, 2, 0, 0, 3],
+           [2, 2, 2, 0, 0, 0],
+           [0, 0, 1, 1, 0, 0],
+           [0, 0, 1, 1, 0, 0],
+           [0, 0, 0, 0, 1, 0],
+           [0, 0, 0, 0, 0, 0]])
+    >>> ndimage.find_objects(a)
+    [(slice(2, 5, None), slice(2, 5, None)),
+     (slice(0, 2, None), slice(0, 3, None)),
+     (slice(0, 1, None), slice(5, 6, None))]
+    >>> ndimage.find_objects(a, max_label=2)
+    [(slice(2, 5, None), slice(2, 5, None)), (slice(0, 2, None), slice(0, 3, None))]
+    >>> ndimage.find_objects(a == 1, max_label=2)
+    [(slice(2, 5, None), slice(2, 5, None)), None]
+
+    >>> loc = ndimage.find_objects(a)[0]
+    >>> a[loc]
+    array([[1, 1, 0],
+           [1, 1, 0],
+           [0, 0, 1]])
+
+    """
+    input = np.asarray(input)
+    if np.iscomplexobj(input):
+        raise TypeError('Complex type not supported')
+
+    if max_label < 1:
+        max_label = input.max()
+
+    return _nd_image.find_objects(input, max_label)
+
+
+def value_indices(arr, *, ignore_value=None):
+    """
+    Find indices of each distinct value in given array.
+
+    Parameters
+    ----------
+    arr : ndarray of ints
+        Array containing integer values.
+    ignore_value : int, optional
+        This value will be ignored in searching the `arr` array. If not
+        given, all values found will be included in output. Default
+        is None.
+
+    Returns
+    -------
+    indices : dictionary
+        A Python dictionary of array indices for each distinct value. The
+        dictionary is keyed by the distinct values, the entries are array
+        index tuples covering all occurrences of the value within the
+        array.
+
+        This dictionary can occupy significant memory, usually several times
+        the size of the input array.
+
+    See Also
+    --------
+    label, maximum, median, minimum_position, extrema, sum, mean, variance,
+    standard_deviation, numpy.where, numpy.unique
+
+    Notes
+    -----
+    For a small array with few distinct values, one might use
+    `numpy.unique()` to find all possible values, and ``(arr == val)`` to
+    locate each value within that array. However, for large arrays,
+    with many distinct values, this can become extremely inefficient,
+    as locating each value would require a new search through the entire
+    array. Using this function, there is essentially one search, with
+    the indices saved for all distinct values.
+
+    This is useful when matching a categorical image (e.g. a segmentation
+    or classification) to an associated image of other data, allowing
+    any per-class statistic(s) to then be calculated. Provides a
+    more flexible alternative to functions like ``scipy.ndimage.mean()``
+    and ``scipy.ndimage.variance()``.
+
+    Some other closely related functionality, with different strengths and
+    weaknesses, can also be found in ``scipy.stats.binned_statistic()`` and
+    the `scikit-image `_ function
+    ``skimage.measure.regionprops()``.
+
+    Note for IDL users: this provides functionality equivalent to IDL's
+    REVERSE_INDICES option (as per the IDL documentation for the
+    `HISTOGRAM `_
+    function).
+
+    .. versionadded:: 1.10.0
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy import ndimage
+    >>> a = np.zeros((6, 6), dtype=int)
+    >>> a[2:4, 2:4] = 1
+    >>> a[4, 4] = 1
+    >>> a[:2, :3] = 2
+    >>> a[0, 5] = 3
+    >>> a
+    array([[2, 2, 2, 0, 0, 3],
+           [2, 2, 2, 0, 0, 0],
+           [0, 0, 1, 1, 0, 0],
+           [0, 0, 1, 1, 0, 0],
+           [0, 0, 0, 0, 1, 0],
+           [0, 0, 0, 0, 0, 0]])
+    >>> val_indices = ndimage.value_indices(a)
+
+    The dictionary `val_indices` will have an entry for each distinct
+    value in the input array.
+
+    >>> val_indices.keys()
+    dict_keys([np.int64(0), np.int64(1), np.int64(2), np.int64(3)])
+
+    The entry for each value is an index tuple, locating the elements
+    with that value.
+
+    >>> ndx1 = val_indices[1]
+    >>> ndx1
+    (array([2, 2, 3, 3, 4]), array([2, 3, 2, 3, 4]))
+
+    This can be used to index into the original array, or any other
+    array with the same shape.
+
+    >>> a[ndx1]
+    array([1, 1, 1, 1, 1])
+
+    If the zeros were to be ignored, then the resulting dictionary
+    would no longer have an entry for zero.
+
+    >>> val_indices = ndimage.value_indices(a, ignore_value=0)
+    >>> val_indices.keys()
+    dict_keys([np.int64(1), np.int64(2), np.int64(3)])
+
+    """
+    # Cope with ignore_value being None, without too much extra complexity
+    # in the C code. If not None, the value is passed in as a numpy array
+    # with the same dtype as arr.
+    ignore_value_arr = np.zeros((1,), dtype=arr.dtype)
+    ignoreIsNone = (ignore_value is None)
+    if not ignoreIsNone:
+        ignore_value_arr[0] = ignore_value_arr.dtype.type(ignore_value)
+
+    val_indices = _nd_image.value_indices(arr, ignoreIsNone, ignore_value_arr)
+    return val_indices
+
+
+def labeled_comprehension(input, labels, index, func, out_dtype, default,
+                          pass_positions=False):
+    """
+    Roughly equivalent to [func(input[labels == i]) for i in index].
+
+    Sequentially applies an arbitrary function (that works on array_like input)
+    to subsets of an N-D image array specified by `labels` and `index`.
+    The option exists to provide the function with positional parameters as the
+    second argument.
+
+    Parameters
+    ----------
+    input : array_like
+        Data from which to select `labels` to process.
+    labels : array_like or None
+        Labels to objects in `input`.
+        If not None, array must be same shape as `input`.
+        If None, `func` is applied to raveled `input`.
+    index : int, sequence of ints or None
+        Subset of `labels` to which to apply `func`.
+        If a scalar, a single value is returned.
+        If None, `func` is applied to all non-zero values of `labels`.
+    func : callable
+        Python function to apply to `labels` from `input`.
+    out_dtype : dtype
+        Dtype to use for `result`.
+    default : int, float or None
+        Default return value when a element of `index` does not exist
+        in `labels`.
+    pass_positions : bool, optional
+        If True, pass linear indices to `func` as a second argument.
+        Default is False.
+
+    Returns
+    -------
+    result : ndarray
+        Result of applying `func` to each of `labels` to `input` in `index`.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> a = np.array([[1, 2, 0, 0],
+    ...               [5, 3, 0, 4],
+    ...               [0, 0, 0, 7],
+    ...               [9, 3, 0, 0]])
+    >>> from scipy import ndimage
+    >>> lbl, nlbl = ndimage.label(a)
+    >>> lbls = np.arange(1, nlbl+1)
+    >>> ndimage.labeled_comprehension(a, lbl, lbls, np.mean, float, 0)
+    array([ 2.75,  5.5 ,  6.  ])
+
+    Falling back to `default`:
+
+    >>> lbls = np.arange(1, nlbl+2)
+    >>> ndimage.labeled_comprehension(a, lbl, lbls, np.mean, float, -1)
+    array([ 2.75,  5.5 ,  6.  , -1.  ])
+
+    Passing positions:
+
+    >>> def fn(val, pos):
+    ...     print("fn says: %s : %s" % (val, pos))
+    ...     return (val.sum()) if (pos.sum() % 2 == 0) else (-val.sum())
+    ...
+    >>> ndimage.labeled_comprehension(a, lbl, lbls, fn, float, 0, True)
+    fn says: [1 2 5 3] : [0 1 4 5]
+    fn says: [4 7] : [ 7 11]
+    fn says: [9 3] : [12 13]
+    array([ 11.,  11., -12.,   0.])
+
+    """
+
+    as_scalar = np.isscalar(index)
+    input = np.asarray(input)
+
+    if pass_positions:
+        positions = np.arange(input.size).reshape(input.shape)
+
+    if labels is None:
+        if index is not None:
+            raise ValueError("index without defined labels")
+        if not pass_positions:
+            return func(input.ravel())
+        else:
+            return func(input.ravel(), positions.ravel())
+
+    try:
+        input, labels = np.broadcast_arrays(input, labels)
+    except ValueError as e:
+        raise ValueError("input and labels must have the same shape "
+                            "(excepting dimensions with width 1)") from e
+
+    if index is None:
+        if not pass_positions:
+            return func(input[labels > 0])
+        else:
+            return func(input[labels > 0], positions[labels > 0])
+
+    index = np.atleast_1d(index)
+    if np.any(index.astype(labels.dtype).astype(index.dtype) != index):
+        raise ValueError(f"Cannot convert index values from <{index.dtype}> to "
+                         f"<{labels.dtype}> (labels' type) without loss of precision")
+
+    index = index.astype(labels.dtype)
+
+    # optimization: find min/max in index,
+    # and select those parts of labels, input, and positions
+    lo = index.min()
+    hi = index.max()
+    mask = (labels >= lo) & (labels <= hi)
+
+    # this also ravels the arrays
+    labels = labels[mask]
+    input = input[mask]
+    if pass_positions:
+        positions = positions[mask]
+
+    # sort everything by labels
+    label_order = labels.argsort()
+    labels = labels[label_order]
+    input = input[label_order]
+    if pass_positions:
+        positions = positions[label_order]
+
+    index_order = index.argsort()
+    sorted_index = index[index_order]
+
+    def do_map(inputs, output):
+        """labels must be sorted"""
+        nidx = sorted_index.size
+
+        # Find boundaries for each stretch of constant labels
+        # This could be faster, but we already paid N log N to sort labels.
+        lo = np.searchsorted(labels, sorted_index, side='left')
+        hi = np.searchsorted(labels, sorted_index, side='right')
+
+        for i, l, h in zip(range(nidx), lo, hi):
+            if l == h:
+                continue
+            output[i] = func(*[inp[l:h] for inp in inputs])
+
+    temp = np.empty(index.shape, out_dtype)
+    temp[:] = default
+    if not pass_positions:
+        do_map([input], temp)
+    else:
+        do_map([input, positions], temp)
+
+    output = np.zeros(index.shape, out_dtype)
+    output[index_order] = temp
+    if as_scalar:
+        output = output[0]
+
+    return output
+
+
+def _safely_castable_to_int(dt):
+    """Test whether the NumPy data type `dt` can be safely cast to an int."""
+    int_size = np.dtype(int).itemsize
+    safe = ((np.issubdtype(dt, np.signedinteger) and dt.itemsize <= int_size) or
+            (np.issubdtype(dt, np.unsignedinteger) and dt.itemsize < int_size))
+    return safe
+
+
+def _stats(input, labels=None, index=None, centered=False):
+    """Count, sum, and optionally compute (sum - centre)^2 of input by label
+
+    Parameters
+    ----------
+    input : array_like, N-D
+        The input data to be analyzed.
+    labels : array_like (N-D), optional
+        The labels of the data in `input`. This array must be broadcast
+        compatible with `input`; typically, it is the same shape as `input`.
+        If `labels` is None, all nonzero values in `input` are treated as
+        the single labeled group.
+    index : label or sequence of labels, optional
+        These are the labels of the groups for which the stats are computed.
+        If `index` is None, the stats are computed for the single group where
+        `labels` is greater than 0.
+    centered : bool, optional
+        If True, the centered sum of squares for each labeled group is
+        also returned. Default is False.
+
+    Returns
+    -------
+    counts : int or ndarray of ints
+        The number of elements in each labeled group.
+    sums : scalar or ndarray of scalars
+        The sums of the values in each labeled group.
+    sums_c : scalar or ndarray of scalars, optional
+        The sums of mean-centered squares of the values in each labeled group.
+        This is only returned if `centered` is True.
+
+    """
+    def single_group(vals):
+        if centered:
+            vals_c = vals - vals.mean()
+            return vals.size, vals.sum(), (vals_c * vals_c.conjugate()).sum()
+        else:
+            return vals.size, vals.sum()
+
+    if labels is None:
+        return single_group(input)
+
+    # ensure input and labels match sizes
+    input, labels = np.broadcast_arrays(input, labels)
+
+    if index is None:
+        return single_group(input[labels > 0])
+
+    if np.isscalar(index):
+        return single_group(input[labels == index])
+
+    def _sum_centered(labels):
+        # `labels` is expected to be an ndarray with the same shape as `input`.
+        # It must contain the label indices (which are not necessarily the labels
+        # themselves).
+        means = sums / counts
+        centered_input = input - means[labels]
+        # bincount expects 1-D inputs, so we ravel the arguments.
+        bc = np.bincount(labels.ravel(),
+                              weights=(centered_input *
+                                       centered_input.conjugate()).ravel())
+        return bc
+
+    # Remap labels to unique integers if necessary, or if the largest
+    # label is larger than the number of values.
+
+    if (not _safely_castable_to_int(labels.dtype) or
+            labels.min() < 0 or labels.max() > labels.size):
+        # Use np.unique to generate the label indices.  `new_labels` will
+        # be 1-D, but it should be interpreted as the flattened N-D array of
+        # label indices.
+        unique_labels, new_labels = np.unique(labels, return_inverse=True)
+        new_labels = np.reshape(new_labels, (-1,))  # flatten, since it may be >1-D
+        counts = np.bincount(new_labels)
+        sums = np.bincount(new_labels, weights=input.ravel())
+        if centered:
+            # Compute the sum of the mean-centered squares.
+            # We must reshape new_labels to the N-D shape of `input` before
+            # passing it _sum_centered.
+            sums_c = _sum_centered(new_labels.reshape(labels.shape))
+        idxs = np.searchsorted(unique_labels, index)
+        # make all of idxs valid
+        idxs[idxs >= unique_labels.size] = 0
+        found = (unique_labels[idxs] == index)
+    else:
+        # labels are an integer type allowed by bincount, and there aren't too
+        # many, so call bincount directly.
+        counts = np.bincount(labels.ravel())
+        sums = np.bincount(labels.ravel(), weights=input.ravel())
+        if centered:
+            sums_c = _sum_centered(labels)
+        # make sure all index values are valid
+        idxs = np.asanyarray(index, np.int_).copy()
+        found = (idxs >= 0) & (idxs < counts.size)
+        idxs[~found] = 0
+
+    counts = counts[idxs]
+    counts[~found] = 0
+    sums = sums[idxs]
+    sums[~found] = 0
+
+    if not centered:
+        return (counts, sums)
+    else:
+        sums_c = sums_c[idxs]
+        sums_c[~found] = 0
+        return (counts, sums, sums_c)
+
+
+def sum(input, labels=None, index=None):
+    """
+    Calculate the sum of the values of the array.
+
+    Notes
+    -----
+    This is an alias for `ndimage.sum_labels` kept for backwards compatibility
+    reasons, for new code please prefer `sum_labels`.  See the `sum_labels`
+    docstring for more details.
+
+    """
+    return sum_labels(input, labels, index)
+
+
+def sum_labels(input, labels=None, index=None):
+    """
+    Calculate the sum of the values of the array.
+
+    Parameters
+    ----------
+    input : array_like
+        Values of `input` inside the regions defined by `labels`
+        are summed together.
+    labels : array_like of ints, optional
+        Assign labels to the values of the array. Has to have the same shape as
+        `input`.
+    index : array_like, optional
+        A single label number or a sequence of label numbers of
+        the objects to be measured.
+
+    Returns
+    -------
+    sum : ndarray or scalar
+        An array of the sums of values of `input` inside the regions defined
+        by `labels` with the same shape as `index`. If 'index' is None or scalar,
+        a scalar is returned.
+
+    See Also
+    --------
+    mean, median
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> input =  [0,1,2,3]
+    >>> labels = [1,1,2,2]
+    >>> ndimage.sum_labels(input, labels, index=[1,2])
+    [1.0, 5.0]
+    >>> ndimage.sum_labels(input, labels, index=1)
+    1
+    >>> ndimage.sum_labels(input, labels)
+    6
+
+
+    """
+    count, sum = _stats(input, labels, index)
+    return sum
+
+
+def mean(input, labels=None, index=None):
+    """
+    Calculate the mean of the values of an array at labels.
+
+    Parameters
+    ----------
+    input : array_like
+        Array on which to compute the mean of elements over distinct
+        regions.
+    labels : array_like, optional
+        Array of labels of same shape, or broadcastable to the same shape as
+        `input`. All elements sharing the same label form one region over
+        which the mean of the elements is computed.
+    index : int or sequence of ints, optional
+        Labels of the objects over which the mean is to be computed.
+        Default is None, in which case the mean for all values where label is
+        greater than 0 is calculated.
+
+    Returns
+    -------
+    out : list
+        Sequence of same length as `index`, with the mean of the different
+        regions labeled by the labels in `index`.
+
+    See Also
+    --------
+    variance, standard_deviation, minimum, maximum, sum, label
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.arange(25).reshape((5,5))
+    >>> labels = np.zeros_like(a)
+    >>> labels[3:5,3:5] = 1
+    >>> index = np.unique(labels)
+    >>> labels
+    array([[0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0],
+           [0, 0, 0, 1, 1],
+           [0, 0, 0, 1, 1]])
+    >>> index
+    array([0, 1])
+    >>> ndimage.mean(a, labels=labels, index=index)
+    [10.285714285714286, 21.0]
+
+    """
+
+    count, sum = _stats(input, labels, index)
+    return sum / np.asanyarray(count).astype(np.float64)
+
+
+def variance(input, labels=None, index=None):
+    """
+    Calculate the variance of the values of an N-D image array, optionally at
+    specified sub-regions.
+
+    Parameters
+    ----------
+    input : array_like
+        Nd-image data to process.
+    labels : array_like, optional
+        Labels defining sub-regions in `input`.
+        If not None, must be same shape as `input`.
+    index : int or sequence of ints, optional
+        `labels` to include in output.  If None (default), all values where
+        `labels` is non-zero are used.
+
+    Returns
+    -------
+    variance : float or ndarray
+        Values of variance, for each sub-region if `labels` and `index` are
+        specified.
+
+    See Also
+    --------
+    label, standard_deviation, maximum, minimum, extrema
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> a = np.array([[1, 2, 0, 0],
+    ...               [5, 3, 0, 4],
+    ...               [0, 0, 0, 7],
+    ...               [9, 3, 0, 0]])
+    >>> from scipy import ndimage
+    >>> ndimage.variance(a)
+    7.609375
+
+    Features to process can be specified using `labels` and `index`:
+
+    >>> lbl, nlbl = ndimage.label(a)
+    >>> ndimage.variance(a, lbl, index=np.arange(1, nlbl+1))
+    array([ 2.1875,  2.25  ,  9.    ])
+
+    If no index is given, all non-zero `labels` are processed:
+
+    >>> ndimage.variance(a, lbl)
+    6.1875
+
+    """
+    count, sum, sum_c_sq = _stats(input, labels, index, centered=True)
+    return sum_c_sq / np.asanyarray(count).astype(float)
+
+
+def standard_deviation(input, labels=None, index=None):
+    """
+    Calculate the standard deviation of the values of an N-D image array,
+    optionally at specified sub-regions.
+
+    Parameters
+    ----------
+    input : array_like
+        N-D image data to process.
+    labels : array_like, optional
+        Labels to identify sub-regions in `input`.
+        If not None, must be same shape as `input`.
+    index : int or sequence of ints, optional
+        `labels` to include in output. If None (default), all values where
+        `labels` is non-zero are used.
+
+    Returns
+    -------
+    standard_deviation : float or ndarray
+        Values of standard deviation, for each sub-region if `labels` and
+        `index` are specified.
+
+    See Also
+    --------
+    label, variance, maximum, minimum, extrema
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> a = np.array([[1, 2, 0, 0],
+    ...               [5, 3, 0, 4],
+    ...               [0, 0, 0, 7],
+    ...               [9, 3, 0, 0]])
+    >>> from scipy import ndimage
+    >>> ndimage.standard_deviation(a)
+    2.7585095613392387
+
+    Features to process can be specified using `labels` and `index`:
+
+    >>> lbl, nlbl = ndimage.label(a)
+    >>> ndimage.standard_deviation(a, lbl, index=np.arange(1, nlbl+1))
+    array([ 1.479,  1.5  ,  3.   ])
+
+    If no index is given, non-zero `labels` are processed:
+
+    >>> ndimage.standard_deviation(a, lbl)
+    2.4874685927665499
+
+    """
+    return np.sqrt(variance(input, labels, index))
+
+
+def _select(input, labels=None, index=None, find_min=False, find_max=False,
+            find_min_positions=False, find_max_positions=False,
+            find_median=False):
+    """Returns min, max, or both, plus their positions (if requested), and
+    median."""
+
+    input = np.asanyarray(input)
+
+    find_positions = find_min_positions or find_max_positions
+    positions = None
+    if find_positions:
+        positions = np.arange(input.size).reshape(input.shape)
+
+    def single_group(vals, positions):
+        result = []
+        if find_min:
+            result += [vals.min()]
+        if find_min_positions:
+            result += [positions[vals == vals.min()][0]]
+        if find_max:
+            result += [vals.max()]
+        if find_max_positions:
+            result += [positions[vals == vals.max()][0]]
+        if find_median:
+            result += [np.median(vals)]
+        return result
+
+    if labels is None:
+        return single_group(input, positions)
+
+    # ensure input and labels match sizes
+    input, labels = np.broadcast_arrays(input, labels)
+
+    if index is None:
+        mask = (labels > 0)
+        masked_positions = None
+        if find_positions:
+            masked_positions = positions[mask]
+        return single_group(input[mask], masked_positions)
+
+    if np.isscalar(index):
+        mask = (labels == index)
+        masked_positions = None
+        if find_positions:
+            masked_positions = positions[mask]
+        return single_group(input[mask], masked_positions)
+
+    # remap labels to unique integers if necessary, or if the largest
+    # label is larger than the number of values.
+    if (not _safely_castable_to_int(labels.dtype) or
+            labels.min() < 0 or labels.max() > labels.size):
+        # remap labels, and indexes
+        unique_labels, labels = np.unique(labels, return_inverse=True)
+        idxs = np.searchsorted(unique_labels, index)
+
+        # make all of idxs valid
+        idxs[idxs >= unique_labels.size] = 0
+        found = (unique_labels[idxs] == index)
+    else:
+        # labels are an integer type, and there aren't too many
+        idxs = np.asanyarray(index, np.int_).copy()
+        found = (idxs >= 0) & (idxs <= labels.max())
+
+    idxs[~ found] = labels.max() + 1
+
+    if find_median:
+        order = np.lexsort((input.ravel(), labels.ravel()))
+    else:
+        order = input.ravel().argsort()
+    input = input.ravel()[order]
+    labels = labels.ravel()[order]
+    if find_positions:
+        positions = positions.ravel()[order]
+
+    result = []
+    if find_min:
+        mins = np.zeros(labels.max() + 2, input.dtype)
+        mins[labels[::-1]] = input[::-1]
+        result += [mins[idxs]]
+    if find_min_positions:
+        minpos = np.zeros(labels.max() + 2, int)
+        minpos[labels[::-1]] = positions[::-1]
+        result += [minpos[idxs]]
+    if find_max:
+        maxs = np.zeros(labels.max() + 2, input.dtype)
+        maxs[labels] = input
+        result += [maxs[idxs]]
+    if find_max_positions:
+        maxpos = np.zeros(labels.max() + 2, int)
+        maxpos[labels] = positions
+        result += [maxpos[idxs]]
+    if find_median:
+        locs = np.arange(len(labels))
+        lo = np.zeros(labels.max() + 2, np.int_)
+        lo[labels[::-1]] = locs[::-1]
+        hi = np.zeros(labels.max() + 2, np.int_)
+        hi[labels] = locs
+        lo = lo[idxs]
+        hi = hi[idxs]
+        # lo is an index to the lowest value in input for each label,
+        # hi is an index to the largest value.
+        # move them to be either the same ((hi - lo) % 2 == 0) or next
+        # to each other ((hi - lo) % 2 == 1), then average.
+        step = (hi - lo) // 2
+        lo += step
+        hi -= step
+        if (np.issubdtype(input.dtype, np.integer)
+                or np.issubdtype(input.dtype, np.bool_)):
+            # avoid integer overflow or boolean addition (gh-12836)
+            result += [(input[lo].astype('d') + input[hi].astype('d')) / 2.0]
+        else:
+            result += [(input[lo] + input[hi]) / 2.0]
+
+    return result
+
+
+def minimum(input, labels=None, index=None):
+    """
+    Calculate the minimum of the values of an array over labeled regions.
+
+    Parameters
+    ----------
+    input : array_like
+        Array_like of values. For each region specified by `labels`, the
+        minimal values of `input` over the region is computed.
+    labels : array_like, optional
+        An array_like of integers marking different regions over which the
+        minimum value of `input` is to be computed. `labels` must have the
+        same shape as `input`. If `labels` is not specified, the minimum
+        over the whole array is returned.
+    index : array_like, optional
+        A list of region labels that are taken into account for computing the
+        minima. If index is None, the minimum over all elements where `labels`
+        is non-zero is returned.
+
+    Returns
+    -------
+    minimum : float or list of floats
+        List of minima of `input` over the regions determined by `labels` and
+        whose index is in `index`. If `index` or `labels` are not specified, a
+        float is returned: the minimal value of `input` if `labels` is None,
+        and the minimal value of elements where `labels` is greater than zero
+        if `index` is None.
+
+    See Also
+    --------
+    label, maximum, median, minimum_position, extrema, sum, mean, variance,
+    standard_deviation
+
+    Notes
+    -----
+    The function returns a Python list and not a NumPy array, use
+    `np.array` to convert the list to an array.
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.array([[1, 2, 0, 0],
+    ...               [5, 3, 0, 4],
+    ...               [0, 0, 0, 7],
+    ...               [9, 3, 0, 0]])
+    >>> labels, labels_nb = ndimage.label(a)
+    >>> labels
+    array([[1, 1, 0, 0],
+           [1, 1, 0, 2],
+           [0, 0, 0, 2],
+           [3, 3, 0, 0]])
+    >>> ndimage.minimum(a, labels=labels, index=np.arange(1, labels_nb + 1))
+    [1.0, 4.0, 3.0]
+    >>> ndimage.minimum(a)
+    0.0
+    >>> ndimage.minimum(a, labels=labels)
+    1.0
+
+    """
+    return _select(input, labels, index, find_min=True)[0]
+
+
+def maximum(input, labels=None, index=None):
+    """
+    Calculate the maximum of the values of an array over labeled regions.
+
+    Parameters
+    ----------
+    input : array_like
+        Array_like of values. For each region specified by `labels`, the
+        maximal values of `input` over the region is computed.
+    labels : array_like, optional
+        An array of integers marking different regions over which the
+        maximum value of `input` is to be computed. `labels` must have the
+        same shape as `input`. If `labels` is not specified, the maximum
+        over the whole array is returned.
+    index : array_like, optional
+        A list of region labels that are taken into account for computing the
+        maxima. If index is None, the maximum over all elements where `labels`
+        is non-zero is returned.
+
+    Returns
+    -------
+    output : float or list of floats
+        List of maxima of `input` over the regions determined by `labels` and
+        whose index is in `index`. If `index` or `labels` are not specified, a
+        float is returned: the maximal value of `input` if `labels` is None,
+        and the maximal value of elements where `labels` is greater than zero
+        if `index` is None.
+
+    See Also
+    --------
+    label, minimum, median, maximum_position, extrema, sum, mean, variance,
+    standard_deviation
+
+    Notes
+    -----
+    The function returns a Python list and not a NumPy array, use
+    `np.array` to convert the list to an array.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> a = np.arange(16).reshape((4,4))
+    >>> a
+    array([[ 0,  1,  2,  3],
+           [ 4,  5,  6,  7],
+           [ 8,  9, 10, 11],
+           [12, 13, 14, 15]])
+    >>> labels = np.zeros_like(a)
+    >>> labels[:2,:2] = 1
+    >>> labels[2:, 1:3] = 2
+    >>> labels
+    array([[1, 1, 0, 0],
+           [1, 1, 0, 0],
+           [0, 2, 2, 0],
+           [0, 2, 2, 0]])
+    >>> from scipy import ndimage
+    >>> ndimage.maximum(a)
+    15.0
+    >>> ndimage.maximum(a, labels=labels, index=[1,2])
+    [5.0, 14.0]
+    >>> ndimage.maximum(a, labels=labels)
+    14.0
+
+    >>> b = np.array([[1, 2, 0, 0],
+    ...               [5, 3, 0, 4],
+    ...               [0, 0, 0, 7],
+    ...               [9, 3, 0, 0]])
+    >>> labels, labels_nb = ndimage.label(b)
+    >>> labels
+    array([[1, 1, 0, 0],
+           [1, 1, 0, 2],
+           [0, 0, 0, 2],
+           [3, 3, 0, 0]])
+    >>> ndimage.maximum(b, labels=labels, index=np.arange(1, labels_nb + 1))
+    [5.0, 7.0, 9.0]
+
+    """
+    return _select(input, labels, index, find_max=True)[0]
+
+
+def median(input, labels=None, index=None):
+    """
+    Calculate the median of the values of an array over labeled regions.
+
+    Parameters
+    ----------
+    input : array_like
+        Array_like of values. For each region specified by `labels`, the
+        median value of `input` over the region is computed.
+    labels : array_like, optional
+        An array_like of integers marking different regions over which the
+        median value of `input` is to be computed. `labels` must have the
+        same shape as `input`. If `labels` is not specified, the median
+        over the whole array is returned.
+    index : array_like, optional
+        A list of region labels that are taken into account for computing the
+        medians. If index is None, the median over all elements where `labels`
+        is non-zero is returned.
+
+    Returns
+    -------
+    median : float or list of floats
+        List of medians of `input` over the regions determined by `labels` and
+        whose index is in `index`. If `index` or `labels` are not specified, a
+        float is returned: the median value of `input` if `labels` is None,
+        and the median value of elements where `labels` is greater than zero
+        if `index` is None.
+
+    See Also
+    --------
+    label, minimum, maximum, extrema, sum, mean, variance, standard_deviation
+
+    Notes
+    -----
+    The function returns a Python list and not a NumPy array, use
+    `np.array` to convert the list to an array.
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.array([[1, 2, 0, 1],
+    ...               [5, 3, 0, 4],
+    ...               [0, 0, 0, 7],
+    ...               [9, 3, 0, 0]])
+    >>> labels, labels_nb = ndimage.label(a)
+    >>> labels
+    array([[1, 1, 0, 2],
+           [1, 1, 0, 2],
+           [0, 0, 0, 2],
+           [3, 3, 0, 0]])
+    >>> ndimage.median(a, labels=labels, index=np.arange(1, labels_nb + 1))
+    [2.5, 4.0, 6.0]
+    >>> ndimage.median(a)
+    1.0
+    >>> ndimage.median(a, labels=labels)
+    3.0
+
+    """
+    return _select(input, labels, index, find_median=True)[0]
+
+
+def minimum_position(input, labels=None, index=None):
+    """
+    Find the positions of the minimums of the values of an array at labels.
+
+    Parameters
+    ----------
+    input : array_like
+        Array_like of values.
+    labels : array_like, optional
+        An array of integers marking different regions over which the
+        position of the minimum value of `input` is to be computed.
+        `labels` must have the same shape as `input`. If `labels` is not
+        specified, the location of the first minimum over the whole
+        array is returned.
+
+        The `labels` argument only works when `index` is specified.
+    index : array_like, optional
+        A list of region labels that are taken into account for finding the
+        location of the minima. If `index` is None, the ``first`` minimum
+        over all elements where `labels` is non-zero is returned.
+
+        The `index` argument only works when `labels` is specified.
+
+    Returns
+    -------
+    output : list of tuples of ints
+        Tuple of ints or list of tuples of ints that specify the location
+        of minima of `input` over the regions determined by `labels` and
+        whose index is in `index`.
+
+        If `index` or `labels` are not specified, a tuple of ints is
+        returned specifying the location of the first minimal value of `input`.
+
+    See Also
+    --------
+    label, minimum, median, maximum_position, extrema, sum, mean, variance,
+    standard_deviation
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> a = np.array([[10, 20, 30],
+    ...               [40, 80, 100],
+    ...               [1, 100, 200]])
+    >>> b = np.array([[1, 2, 0, 1],
+    ...               [5, 3, 0, 4],
+    ...               [0, 0, 0, 7],
+    ...               [9, 3, 0, 0]])
+
+    >>> from scipy import ndimage
+
+    >>> ndimage.minimum_position(a)
+    (2, 0)
+    >>> ndimage.minimum_position(b)
+    (0, 2)
+
+    Features to process can be specified using `labels` and `index`:
+
+    >>> label, pos = ndimage.label(a)
+    >>> ndimage.minimum_position(a, label, index=np.arange(1, pos+1))
+    [(2, 0)]
+
+    >>> label, pos = ndimage.label(b)
+    >>> ndimage.minimum_position(b, label, index=np.arange(1, pos+1))
+    [(0, 0), (0, 3), (3, 1)]
+
+    """
+    dims = np.array(np.asarray(input).shape)
+    # see np.unravel_index to understand this line.
+    dim_prod = np.cumprod([1] + list(dims[:0:-1]))[::-1]
+
+    result = _select(input, labels, index, find_min_positions=True)[0]
+
+    if np.isscalar(result):
+        return tuple((result // dim_prod) % dims)
+
+    return [tuple(v) for v in (result.reshape(-1, 1) // dim_prod) % dims]
+
+
+def maximum_position(input, labels=None, index=None):
+    """
+    Find the positions of the maximums of the values of an array at labels.
+
+    For each region specified by `labels`, the position of the maximum
+    value of `input` within the region is returned.
+
+    Parameters
+    ----------
+    input : array_like
+        Array_like of values.
+    labels : array_like, optional
+        An array of integers marking different regions over which the
+        position of the maximum value of `input` is to be computed.
+        `labels` must have the same shape as `input`. If `labels` is not
+        specified, the location of the first maximum over the whole
+        array is returned.
+
+        The `labels` argument only works when `index` is specified.
+    index : array_like, optional
+        A list of region labels that are taken into account for finding the
+        location of the maxima. If `index` is None, the first maximum
+        over all elements where `labels` is non-zero is returned.
+
+        The `index` argument only works when `labels` is specified.
+
+    Returns
+    -------
+    output : list of tuples of ints
+        List of tuples of ints that specify the location of maxima of
+        `input` over the regions determined by `labels` and whose index
+        is in `index`.
+
+        If `index` or `labels` are not specified, a tuple of ints is
+        returned specifying the location of the ``first`` maximal value
+        of `input`.
+
+    See Also
+    --------
+    label, minimum, median, maximum_position, extrema, sum, mean, variance,
+    standard_deviation
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.array([[1, 2, 0, 0],
+    ...               [5, 3, 0, 4],
+    ...               [0, 0, 0, 7],
+    ...               [9, 3, 0, 0]])
+    >>> ndimage.maximum_position(a)
+    (3, 0)
+
+    Features to process can be specified using `labels` and `index`:
+
+    >>> lbl = np.array([[0, 1, 2, 3],
+    ...                 [0, 1, 2, 3],
+    ...                 [0, 1, 2, 3],
+    ...                 [0, 1, 2, 3]])
+    >>> ndimage.maximum_position(a, lbl, 1)
+    (1, 1)
+
+    If no index is given, non-zero `labels` are processed:
+
+    >>> ndimage.maximum_position(a, lbl)
+    (2, 3)
+
+    If there are no maxima, the position of the first element is returned:
+
+    >>> ndimage.maximum_position(a, lbl, 2)
+    (0, 2)
+
+    """
+    dims = np.array(np.asarray(input).shape)
+    # see np.unravel_index to understand this line.
+    dim_prod = np.cumprod([1] + list(dims[:0:-1]))[::-1]
+
+    result = _select(input, labels, index, find_max_positions=True)[0]
+
+    if np.isscalar(result):
+        return tuple((result // dim_prod) % dims)
+
+    return [tuple(v) for v in (result.reshape(-1, 1) // dim_prod) % dims]
+
+
+def extrema(input, labels=None, index=None):
+    """
+    Calculate the minimums and maximums of the values of an array
+    at labels, along with their positions.
+
+    Parameters
+    ----------
+    input : ndarray
+        N-D image data to process.
+    labels : ndarray, optional
+        Labels of features in input.
+        If not None, must be same shape as `input`.
+    index : int or sequence of ints, optional
+        Labels to include in output.  If None (default), all values where
+        non-zero `labels` are used.
+
+    Returns
+    -------
+    minimums, maximums : int or ndarray
+        Values of minimums and maximums in each feature.
+    min_positions, max_positions : tuple or list of tuples
+        Each tuple gives the N-D coordinates of the corresponding minimum
+        or maximum.
+
+    See Also
+    --------
+    maximum, minimum, maximum_position, minimum_position, center_of_mass
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> a = np.array([[1, 2, 0, 0],
+    ...               [5, 3, 0, 4],
+    ...               [0, 0, 0, 7],
+    ...               [9, 3, 0, 0]])
+    >>> from scipy import ndimage
+    >>> ndimage.extrema(a)
+    (0, 9, (0, 2), (3, 0))
+
+    Features to process can be specified using `labels` and `index`:
+
+    >>> lbl, nlbl = ndimage.label(a)
+    >>> ndimage.extrema(a, lbl, index=np.arange(1, nlbl+1))
+    (array([1, 4, 3]),
+     array([5, 7, 9]),
+     [(0, 0), (1, 3), (3, 1)],
+     [(1, 0), (2, 3), (3, 0)])
+
+    If no index is given, non-zero `labels` are processed:
+
+    >>> ndimage.extrema(a, lbl)
+    (1, 9, (0, 0), (3, 0))
+
+    """
+    dims = np.array(np.asarray(input).shape)
+    # see np.unravel_index to understand this line.
+    dim_prod = np.cumprod([1] + list(dims[:0:-1]))[::-1]
+
+    minimums, min_positions, maximums, max_positions = _select(input, labels,
+                                                               index,
+                                                               find_min=True,
+                                                               find_max=True,
+                                                               find_min_positions=True,
+                                                               find_max_positions=True)
+
+    if np.isscalar(minimums):
+        return (minimums, maximums, tuple((min_positions // dim_prod) % dims),
+                tuple((max_positions // dim_prod) % dims))
+
+    min_positions = [
+        tuple(v) for v in (min_positions.reshape(-1, 1) // dim_prod) % dims
+    ]
+    max_positions = [
+        tuple(v) for v in (max_positions.reshape(-1, 1) // dim_prod) % dims
+    ]
+
+    return minimums, maximums, min_positions, max_positions
+
+
+def center_of_mass(input, labels=None, index=None):
+    """
+    Calculate the center of mass of the values of an array at labels.
+
+    Parameters
+    ----------
+    input : ndarray
+        Data from which to calculate center-of-mass. The masses can either
+        be positive or negative.
+    labels : ndarray, optional
+        Labels for objects in `input`, as generated by `ndimage.label`.
+        Only used with `index`. Dimensions must be the same as `input`.
+    index : int or sequence of ints, optional
+        Labels for which to calculate centers-of-mass. If not specified,
+        the combined center of mass of all labels greater than zero
+        will be calculated. Only used with `labels`.
+
+    Returns
+    -------
+    center_of_mass : tuple, or list of tuples
+        Coordinates of centers-of-mass.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> a = np.array(([0,0,0,0],
+    ...               [0,1,1,0],
+    ...               [0,1,1,0],
+    ...               [0,1,1,0]))
+    >>> from scipy import ndimage
+    >>> ndimage.center_of_mass(a)
+    (2.0, 1.5)
+
+    Calculation of multiple objects in an image
+
+    >>> b = np.array(([0,1,1,0],
+    ...               [0,1,0,0],
+    ...               [0,0,0,0],
+    ...               [0,0,1,1],
+    ...               [0,0,1,1]))
+    >>> lbl = ndimage.label(b)[0]
+    >>> ndimage.center_of_mass(b, lbl, [1,2])
+    [(0.33333333333333331, 1.3333333333333333), (3.5, 2.5)]
+
+    Negative masses are also accepted, which can occur for example when
+    bias is removed from measured data due to random noise.
+
+    >>> c = np.array(([-1,0,0,0],
+    ...               [0,-1,-1,0],
+    ...               [0,1,-1,0],
+    ...               [0,1,1,0]))
+    >>> ndimage.center_of_mass(c)
+    (-4.0, 1.0)
+
+    If there are division by zero issues, the function does not raise an
+    error but rather issues a RuntimeWarning before returning inf and/or NaN.
+
+    >>> d = np.array([-1, 1])
+    >>> ndimage.center_of_mass(d)
+    (inf,)
+    """
+    normalizer = sum(input, labels, index)
+    grids = np.ogrid[[slice(0, i) for i in input.shape]]
+
+    results = [sum(input * grids[dir].astype(float), labels, index) / normalizer
+               for dir in range(input.ndim)]
+
+    if np.isscalar(results[0]):
+        return tuple(results)
+
+    return [tuple(v) for v in np.array(results).T]
+
+
+def histogram(input, min, max, bins, labels=None, index=None):
+    """
+    Calculate the histogram of the values of an array, optionally at labels.
+
+    Histogram calculates the frequency of values in an array within bins
+    determined by `min`, `max`, and `bins`. The `labels` and `index`
+    keywords can limit the scope of the histogram to specified sub-regions
+    within the array.
+
+    Parameters
+    ----------
+    input : array_like
+        Data for which to calculate histogram.
+    min, max : int
+        Minimum and maximum values of range of histogram bins.
+    bins : int
+        Number of bins.
+    labels : array_like, optional
+        Labels for objects in `input`.
+        If not None, must be same shape as `input`.
+    index : int or sequence of ints, optional
+        Label or labels for which to calculate histogram. If None, all values
+        where label is greater than zero are used
+
+    Returns
+    -------
+    hist : ndarray
+        Histogram counts.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> a = np.array([[ 0.    ,  0.2146,  0.5962,  0.    ],
+    ...               [ 0.    ,  0.7778,  0.    ,  0.    ],
+    ...               [ 0.    ,  0.    ,  0.    ,  0.    ],
+    ...               [ 0.    ,  0.    ,  0.7181,  0.2787],
+    ...               [ 0.    ,  0.    ,  0.6573,  0.3094]])
+    >>> from scipy import ndimage
+    >>> ndimage.histogram(a, 0, 1, 10)
+    array([13,  0,  2,  1,  0,  1,  1,  2,  0,  0])
+
+    With labels and no indices, non-zero elements are counted:
+
+    >>> lbl, nlbl = ndimage.label(a)
+    >>> ndimage.histogram(a, 0, 1, 10, lbl)
+    array([0, 0, 2, 1, 0, 1, 1, 2, 0, 0])
+
+    Indices can be used to count only certain objects:
+
+    >>> ndimage.histogram(a, 0, 1, 10, lbl, 2)
+    array([0, 0, 1, 1, 0, 0, 1, 1, 0, 0])
+
+    """
+    _bins = np.linspace(min, max, bins + 1)
+
+    def _hist(vals):
+        return np.histogram(vals, _bins)[0]
+
+    return labeled_comprehension(input, labels, index, _hist, object, None,
+                                 pass_positions=False)
+
+
+def watershed_ift(input, markers, structure=None, output=None):
+    """
+    Apply watershed from markers using image foresting transform algorithm.
+
+    Parameters
+    ----------
+    input : array_like
+        Input.
+    markers : array_like
+        Markers are points within each watershed that form the beginning
+        of the process. Negative markers are considered background markers
+        which are processed after the other markers.
+    structure : structure element, optional
+        A structuring element defining the connectivity of the object can be
+        provided. If None, an element is generated with a squared
+        connectivity equal to one.
+    output : ndarray, optional
+        An output array can optionally be provided. The same shape as input.
+
+    Returns
+    -------
+    watershed_ift : ndarray
+        Output.  Same shape as `input`.
+
+    References
+    ----------
+    .. [1] A.X. Falcao, J. Stolfi and R. de Alencar Lotufo, "The image
+           foresting transform: theory, algorithms, and applications",
+           Pattern Analysis and Machine Intelligence, vol. 26, pp. 19-29, 2004.
+
+    """
+    input = np.asarray(input)
+    if input.dtype.type not in [np.uint8, np.uint16]:
+        raise TypeError('only 8 and 16 unsigned inputs are supported')
+
+    if structure is None:
+        structure = _morphology.generate_binary_structure(input.ndim, 1)
+    structure = np.asarray(structure, dtype=bool)
+    if structure.ndim != input.ndim:
+        raise RuntimeError('structure and input must have equal rank')
+    for ii in structure.shape:
+        if ii != 3:
+            raise RuntimeError('structure dimensions must be equal to 3')
+
+    if not structure.flags.contiguous:
+        structure = structure.copy()
+    markers = np.asarray(markers)
+    if input.shape != markers.shape:
+        raise RuntimeError('input and markers must have equal shape')
+
+    integral_types = [np.int8,
+                      np.int16,
+                      np.int32,
+                      np.int64,
+                      np.intc,
+                      np.intp]
+
+    if markers.dtype.type not in integral_types:
+        raise RuntimeError('marker should be of integer type')
+
+    if isinstance(output, np.ndarray):
+        if output.dtype.type not in integral_types:
+            raise RuntimeError('output should be of integer type')
+    else:
+        output = markers.dtype
+
+    output = _ni_support._get_output(output, input)
+    _nd_image.watershed_ift(input, markers, structure, output)
+    return output
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_morphology.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_morphology.py
new file mode 100644
index 0000000000000000000000000000000000000000..22ada0b130f913021207250714ab860f483b3e1e
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_morphology.py
@@ -0,0 +1,2537 @@
+# Copyright (C) 2003-2005 Peter J. Verveer
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above
+#    copyright notice, this list of conditions and the following
+#    disclaimer in the documentation and/or other materials provided
+#    with the distribution.
+#
+# 3. The name of the author may not be used to endorse or promote
+#    products derived from this software without specific prior
+#    written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import warnings
+import operator
+
+import numpy as np
+from . import _ni_support
+from . import _nd_image
+from . import _filters
+
+__all__ = ['iterate_structure', 'generate_binary_structure', 'binary_erosion',
+           'binary_dilation', 'binary_opening', 'binary_closing',
+           'binary_hit_or_miss', 'binary_propagation', 'binary_fill_holes',
+           'grey_erosion', 'grey_dilation', 'grey_opening', 'grey_closing',
+           'morphological_gradient', 'morphological_laplace', 'white_tophat',
+           'black_tophat', 'distance_transform_bf', 'distance_transform_cdt',
+           'distance_transform_edt']
+
+
+def _center_is_true(structure, origin):
+    structure = np.asarray(structure)
+    coor = tuple([oo + ss // 2 for ss, oo in zip(structure.shape,
+                                                 origin)])
+    return bool(structure[coor])
+
+
+def iterate_structure(structure, iterations, origin=None):
+    """
+    Iterate a structure by dilating it with itself.
+
+    Parameters
+    ----------
+    structure : array_like
+       Structuring element (an array of bools, for example), to be dilated with
+       itself.
+    iterations : int
+       number of dilations performed on the structure with itself
+    origin : optional
+        If origin is None, only the iterated structure is returned. If
+        not, a tuple of the iterated structure and the modified origin is
+        returned.
+
+    Returns
+    -------
+    iterate_structure : ndarray of bools
+        A new structuring element obtained by dilating `structure`
+        (`iterations` - 1) times with itself.
+
+    See Also
+    --------
+    generate_binary_structure
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> struct = ndimage.generate_binary_structure(2, 1)
+    >>> struct.astype(int)
+    array([[0, 1, 0],
+           [1, 1, 1],
+           [0, 1, 0]])
+    >>> ndimage.iterate_structure(struct, 2).astype(int)
+    array([[0, 0, 1, 0, 0],
+           [0, 1, 1, 1, 0],
+           [1, 1, 1, 1, 1],
+           [0, 1, 1, 1, 0],
+           [0, 0, 1, 0, 0]])
+    >>> ndimage.iterate_structure(struct, 3).astype(int)
+    array([[0, 0, 0, 1, 0, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 1, 1, 1, 1, 1, 0],
+           [1, 1, 1, 1, 1, 1, 1],
+           [0, 1, 1, 1, 1, 1, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 0, 1, 0, 0, 0]])
+
+    """
+    structure = np.asarray(structure)
+    if iterations < 2:
+        return structure.copy()
+    ni = iterations - 1
+    shape = [ii + ni * (ii - 1) for ii in structure.shape]
+    pos = [ni * (structure.shape[ii] // 2) for ii in range(len(shape))]
+    slc = tuple(slice(pos[ii], pos[ii] + structure.shape[ii], None)
+                for ii in range(len(shape)))
+    out = np.zeros(shape, bool)
+    out[slc] = structure != 0
+    out = binary_dilation(out, structure, iterations=ni)
+    if origin is None:
+        return out
+    else:
+        origin = _ni_support._normalize_sequence(origin, structure.ndim)
+        origin = [iterations * o for o in origin]
+        return out, origin
+
+
+def generate_binary_structure(rank, connectivity):
+    """
+    Generate a binary structure for binary morphological operations.
+
+    Parameters
+    ----------
+    rank : int
+         Number of dimensions of the array to which the structuring element
+         will be applied, as returned by `np.ndim`.
+    connectivity : int
+         `connectivity` determines which elements of the output array belong
+         to the structure, i.e., are considered as neighbors of the central
+         element. Elements up to a squared distance of `connectivity` from
+         the center are considered neighbors. `connectivity` may range from 1
+         (no diagonal elements are neighbors) to `rank` (all elements are
+         neighbors).
+
+    Returns
+    -------
+    output : ndarray of bools
+         Structuring element which may be used for binary morphological
+         operations, with `rank` dimensions and all dimensions equal to 3.
+
+    See Also
+    --------
+    iterate_structure, binary_dilation, binary_erosion
+
+    Notes
+    -----
+    `generate_binary_structure` can only create structuring elements with
+    dimensions equal to 3, i.e., minimal dimensions. For larger structuring
+    elements, that are useful e.g., for eroding large objects, one may either
+    use `iterate_structure`, or create directly custom arrays with
+    numpy functions such as `numpy.ones`.
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> struct = ndimage.generate_binary_structure(2, 1)
+    >>> struct
+    array([[False,  True, False],
+           [ True,  True,  True],
+           [False,  True, False]], dtype=bool)
+    >>> a = np.zeros((5,5))
+    >>> a[2, 2] = 1
+    >>> a
+    array([[ 0.,  0.,  0.,  0.,  0.],
+           [ 0.,  0.,  0.,  0.,  0.],
+           [ 0.,  0.,  1.,  0.,  0.],
+           [ 0.,  0.,  0.,  0.,  0.],
+           [ 0.,  0.,  0.,  0.,  0.]])
+    >>> b = ndimage.binary_dilation(a, structure=struct).astype(a.dtype)
+    >>> b
+    array([[ 0.,  0.,  0.,  0.,  0.],
+           [ 0.,  0.,  1.,  0.,  0.],
+           [ 0.,  1.,  1.,  1.,  0.],
+           [ 0.,  0.,  1.,  0.,  0.],
+           [ 0.,  0.,  0.,  0.,  0.]])
+    >>> ndimage.binary_dilation(b, structure=struct).astype(a.dtype)
+    array([[ 0.,  0.,  1.,  0.,  0.],
+           [ 0.,  1.,  1.,  1.,  0.],
+           [ 1.,  1.,  1.,  1.,  1.],
+           [ 0.,  1.,  1.,  1.,  0.],
+           [ 0.,  0.,  1.,  0.,  0.]])
+    >>> struct = ndimage.generate_binary_structure(2, 2)
+    >>> struct
+    array([[ True,  True,  True],
+           [ True,  True,  True],
+           [ True,  True,  True]], dtype=bool)
+    >>> struct = ndimage.generate_binary_structure(3, 1)
+    >>> struct # no diagonal elements
+    array([[[False, False, False],
+            [False,  True, False],
+            [False, False, False]],
+           [[False,  True, False],
+            [ True,  True,  True],
+            [False,  True, False]],
+           [[False, False, False],
+            [False,  True, False],
+            [False, False, False]]], dtype=bool)
+
+    """
+    if connectivity < 1:
+        connectivity = 1
+    if rank < 1:
+        return np.array(True, dtype=bool)
+    output = np.fabs(np.indices([3] * rank) - 1)
+    output = np.add.reduce(output, 0)
+    return output <= connectivity
+
+
+def _binary_erosion(input, structure, iterations, mask, output,
+                    border_value, origin, invert, brute_force):
+    try:
+        iterations = operator.index(iterations)
+    except TypeError as e:
+        raise TypeError('iterations parameter should be an integer') from e
+
+    input = np.asarray(input)
+    if np.iscomplexobj(input):
+        raise TypeError('Complex type not supported')
+    if structure is None:
+        structure = generate_binary_structure(input.ndim, 1)
+    else:
+        structure = np.asarray(structure, dtype=bool)
+    if structure.ndim != input.ndim:
+        raise RuntimeError('structure and input must have same dimensionality')
+    if not structure.flags.contiguous:
+        structure = structure.copy()
+    if structure.size < 1:
+        raise RuntimeError('structure must not be empty')
+    if mask is not None:
+        mask = np.asarray(mask)
+        if mask.shape != input.shape:
+            raise RuntimeError('mask and input must have equal sizes')
+    origin = _ni_support._normalize_sequence(origin, input.ndim)
+    cit = _center_is_true(structure, origin)
+    if isinstance(output, np.ndarray):
+        if np.iscomplexobj(output):
+            raise TypeError('Complex output type not supported')
+    else:
+        output = bool
+    output = _ni_support._get_output(output, input)
+    temp_needed = np.may_share_memory(input, output)
+    if temp_needed:
+        # input and output arrays cannot share memory
+        temp = output
+        output = _ni_support._get_output(output.dtype, input)
+    if iterations == 1:
+        _nd_image.binary_erosion(input, structure, mask, output,
+                                 border_value, origin, invert, cit, 0)
+    elif cit and not brute_force:
+        changed, coordinate_list = _nd_image.binary_erosion(
+            input, structure, mask, output,
+            border_value, origin, invert, cit, 1)
+        structure = structure[tuple([slice(None, None, -1)] *
+                                    structure.ndim)]
+        for ii in range(len(origin)):
+            origin[ii] = -origin[ii]
+            if not structure.shape[ii] & 1:
+                origin[ii] -= 1
+        if mask is not None:
+            mask = np.asarray(mask, dtype=np.int8)
+        if not structure.flags.contiguous:
+            structure = structure.copy()
+        _nd_image.binary_erosion2(output, structure, mask, iterations - 1,
+                                  origin, invert, coordinate_list)
+    else:
+        tmp_in = np.empty_like(input, dtype=bool)
+        tmp_out = output
+        if iterations >= 1 and not iterations & 1:
+            tmp_in, tmp_out = tmp_out, tmp_in
+        changed = _nd_image.binary_erosion(
+            input, structure, mask, tmp_out,
+            border_value, origin, invert, cit, 0)
+        ii = 1
+        while ii < iterations or (iterations < 1 and changed):
+            tmp_in, tmp_out = tmp_out, tmp_in
+            changed = _nd_image.binary_erosion(
+                tmp_in, structure, mask, tmp_out,
+                border_value, origin, invert, cit, 0)
+            ii += 1
+    if temp_needed:
+        temp[...] = output
+        output = temp
+    return output
+
+
+def binary_erosion(input, structure=None, iterations=1, mask=None, output=None,
+                   border_value=0, origin=0, brute_force=False):
+    """
+    Multidimensional binary erosion with a given structuring element.
+
+    Binary erosion is a mathematical morphology operation used for image
+    processing.
+
+    Parameters
+    ----------
+    input : array_like
+        Binary image to be eroded. Non-zero (True) elements form
+        the subset to be eroded.
+    structure : array_like, optional
+        Structuring element used for the erosion. Non-zero elements are
+        considered True. If no structuring element is provided, an element
+        is generated with a square connectivity equal to one.
+    iterations : int, optional
+        The erosion is repeated `iterations` times (one, by default).
+        If iterations is less than 1, the erosion is repeated until the
+        result does not change anymore.
+    mask : array_like, optional
+        If a mask is given, only those elements with a True value at
+        the corresponding mask element are modified at each iteration.
+    output : ndarray, optional
+        Array of the same shape as input, into which the output is placed.
+        By default, a new array is created.
+    border_value : int (cast to 0 or 1), optional
+        Value at the border in the output array.
+    origin : int or tuple of ints, optional
+        Placement of the filter, by default 0.
+    brute_force : boolean, optional
+        Memory condition: if False, only the pixels whose value was changed in
+        the last iteration are tracked as candidates to be updated (eroded) in
+        the current iteration; if True all pixels are considered as candidates
+        for erosion, regardless of what happened in the previous iteration.
+        False by default.
+
+    Returns
+    -------
+    binary_erosion : ndarray of bools
+        Erosion of the input by the structuring element.
+
+    See Also
+    --------
+    grey_erosion, binary_dilation, binary_closing, binary_opening,
+    generate_binary_structure
+
+    Notes
+    -----
+    Erosion [1]_ is a mathematical morphology operation [2]_ that uses a
+    structuring element for shrinking the shapes in an image. The binary
+    erosion of an image by a structuring element is the locus of the points
+    where a superimposition of the structuring element centered on the point
+    is entirely contained in the set of non-zero elements of the image.
+
+    References
+    ----------
+    .. [1] https://en.wikipedia.org/wiki/Erosion_%28morphology%29
+    .. [2] https://en.wikipedia.org/wiki/Mathematical_morphology
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.zeros((7,7), dtype=int)
+    >>> a[1:6, 2:5] = 1
+    >>> a
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> ndimage.binary_erosion(a).astype(a.dtype)
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 1, 0, 0, 0],
+           [0, 0, 0, 1, 0, 0, 0],
+           [0, 0, 0, 1, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> #Erosion removes objects smaller than the structure
+    >>> ndimage.binary_erosion(a, structure=np.ones((5,5))).astype(a.dtype)
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+
+    """
+    return _binary_erosion(input, structure, iterations, mask,
+                           output, border_value, origin, 0, brute_force)
+
+
+def binary_dilation(input, structure=None, iterations=1, mask=None,
+                    output=None, border_value=0, origin=0,
+                    brute_force=False):
+    """
+    Multidimensional binary dilation with the given structuring element.
+
+    Parameters
+    ----------
+    input : array_like
+        Binary array_like to be dilated. Non-zero (True) elements form
+        the subset to be dilated.
+    structure : array_like, optional
+        Structuring element used for the dilation. Non-zero elements are
+        considered True. If no structuring element is provided an element
+        is generated with a square connectivity equal to one.
+    iterations : int, optional
+        The dilation is repeated `iterations` times (one, by default).
+        If iterations is less than 1, the dilation is repeated until the
+        result does not change anymore. Only an integer of iterations is
+        accepted.
+    mask : array_like, optional
+        If a mask is given, only those elements with a True value at
+        the corresponding mask element are modified at each iteration.
+    output : ndarray, optional
+        Array of the same shape as input, into which the output is placed.
+        By default, a new array is created.
+    border_value : int (cast to 0 or 1), optional
+        Value at the border in the output array.
+    origin : int or tuple of ints, optional
+        Placement of the filter, by default 0.
+    brute_force : boolean, optional
+        Memory condition: if False, only the pixels whose value was changed in
+        the last iteration are tracked as candidates to be updated (dilated)
+        in the current iteration; if True all pixels are considered as
+        candidates for dilation, regardless of what happened in the previous
+        iteration. False by default.
+
+    Returns
+    -------
+    binary_dilation : ndarray of bools
+        Dilation of the input by the structuring element.
+
+    See Also
+    --------
+    grey_dilation, binary_erosion, binary_closing, binary_opening,
+    generate_binary_structure
+
+    Notes
+    -----
+    Dilation [1]_ is a mathematical morphology operation [2]_ that uses a
+    structuring element for expanding the shapes in an image. The binary
+    dilation of an image by a structuring element is the locus of the points
+    covered by the structuring element, when its center lies within the
+    non-zero points of the image.
+
+    References
+    ----------
+    .. [1] https://en.wikipedia.org/wiki/Dilation_%28morphology%29
+    .. [2] https://en.wikipedia.org/wiki/Mathematical_morphology
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.zeros((5, 5))
+    >>> a[2, 2] = 1
+    >>> a
+    array([[ 0.,  0.,  0.,  0.,  0.],
+           [ 0.,  0.,  0.,  0.,  0.],
+           [ 0.,  0.,  1.,  0.,  0.],
+           [ 0.,  0.,  0.,  0.,  0.],
+           [ 0.,  0.,  0.,  0.,  0.]])
+    >>> ndimage.binary_dilation(a)
+    array([[False, False, False, False, False],
+           [False, False,  True, False, False],
+           [False,  True,  True,  True, False],
+           [False, False,  True, False, False],
+           [False, False, False, False, False]], dtype=bool)
+    >>> ndimage.binary_dilation(a).astype(a.dtype)
+    array([[ 0.,  0.,  0.,  0.,  0.],
+           [ 0.,  0.,  1.,  0.,  0.],
+           [ 0.,  1.,  1.,  1.,  0.],
+           [ 0.,  0.,  1.,  0.,  0.],
+           [ 0.,  0.,  0.,  0.,  0.]])
+    >>> # 3x3 structuring element with connectivity 1, used by default
+    >>> struct1 = ndimage.generate_binary_structure(2, 1)
+    >>> struct1
+    array([[False,  True, False],
+           [ True,  True,  True],
+           [False,  True, False]], dtype=bool)
+    >>> # 3x3 structuring element with connectivity 2
+    >>> struct2 = ndimage.generate_binary_structure(2, 2)
+    >>> struct2
+    array([[ True,  True,  True],
+           [ True,  True,  True],
+           [ True,  True,  True]], dtype=bool)
+    >>> ndimage.binary_dilation(a, structure=struct1).astype(a.dtype)
+    array([[ 0.,  0.,  0.,  0.,  0.],
+           [ 0.,  0.,  1.,  0.,  0.],
+           [ 0.,  1.,  1.,  1.,  0.],
+           [ 0.,  0.,  1.,  0.,  0.],
+           [ 0.,  0.,  0.,  0.,  0.]])
+    >>> ndimage.binary_dilation(a, structure=struct2).astype(a.dtype)
+    array([[ 0.,  0.,  0.,  0.,  0.],
+           [ 0.,  1.,  1.,  1.,  0.],
+           [ 0.,  1.,  1.,  1.,  0.],
+           [ 0.,  1.,  1.,  1.,  0.],
+           [ 0.,  0.,  0.,  0.,  0.]])
+    >>> ndimage.binary_dilation(a, structure=struct1,\\
+    ... iterations=2).astype(a.dtype)
+    array([[ 0.,  0.,  1.,  0.,  0.],
+           [ 0.,  1.,  1.,  1.,  0.],
+           [ 1.,  1.,  1.,  1.,  1.],
+           [ 0.,  1.,  1.,  1.,  0.],
+           [ 0.,  0.,  1.,  0.,  0.]])
+
+    """
+    input = np.asarray(input)
+    if structure is None:
+        structure = generate_binary_structure(input.ndim, 1)
+    origin = _ni_support._normalize_sequence(origin, input.ndim)
+    structure = np.asarray(structure)
+    structure = structure[tuple([slice(None, None, -1)] *
+                                structure.ndim)]
+    for ii in range(len(origin)):
+        origin[ii] = -origin[ii]
+        if not structure.shape[ii] & 1:
+            origin[ii] -= 1
+
+    return _binary_erosion(input, structure, iterations, mask,
+                           output, border_value, origin, 1, brute_force)
+
+
+def binary_opening(input, structure=None, iterations=1, output=None,
+                   origin=0, mask=None, border_value=0, brute_force=False):
+    """
+    Multidimensional binary opening with the given structuring element.
+
+    The *opening* of an input image by a structuring element is the
+    *dilation* of the *erosion* of the image by the structuring element.
+
+    Parameters
+    ----------
+    input : array_like
+        Binary array_like to be opened. Non-zero (True) elements form
+        the subset to be opened.
+    structure : array_like, optional
+        Structuring element used for the opening. Non-zero elements are
+        considered True. If no structuring element is provided an element
+        is generated with a square connectivity equal to one (i.e., only
+        nearest neighbors are connected to the center, diagonally-connected
+        elements are not considered neighbors).
+    iterations : int, optional
+        The erosion step of the opening, then the dilation step are each
+        repeated `iterations` times (one, by default). If `iterations` is
+        less than 1, each operation is repeated until the result does
+        not change anymore. Only an integer of iterations is accepted.
+    output : ndarray, optional
+        Array of the same shape as input, into which the output is placed.
+        By default, a new array is created.
+    origin : int or tuple of ints, optional
+        Placement of the filter, by default 0.
+    mask : array_like, optional
+        If a mask is given, only those elements with a True value at
+        the corresponding mask element are modified at each iteration.
+
+        .. versionadded:: 1.1.0
+    border_value : int (cast to 0 or 1), optional
+        Value at the border in the output array.
+
+        .. versionadded:: 1.1.0
+    brute_force : boolean, optional
+        Memory condition: if False, only the pixels whose value was changed in
+        the last iteration are tracked as candidates to be updated in the
+        current iteration; if true all pixels are considered as candidates for
+        update, regardless of what happened in the previous iteration.
+        False by default.
+
+        .. versionadded:: 1.1.0
+
+    Returns
+    -------
+    binary_opening : ndarray of bools
+        Opening of the input by the structuring element.
+
+    See Also
+    --------
+    grey_opening, binary_closing, binary_erosion, binary_dilation,
+    generate_binary_structure
+
+    Notes
+    -----
+    *Opening* [1]_ is a mathematical morphology operation [2]_ that
+    consists in the succession of an erosion and a dilation of the
+    input with the same structuring element. Opening, therefore, removes
+    objects smaller than the structuring element.
+
+    Together with *closing* (`binary_closing`), opening can be used for
+    noise removal.
+
+    References
+    ----------
+    .. [1] https://en.wikipedia.org/wiki/Opening_%28morphology%29
+    .. [2] https://en.wikipedia.org/wiki/Mathematical_morphology
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.zeros((5,5), dtype=int)
+    >>> a[1:4, 1:4] = 1; a[4, 4] = 1
+    >>> a
+    array([[0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 0, 0, 0, 1]])
+    >>> # Opening removes small objects
+    >>> ndimage.binary_opening(a, structure=np.ones((3,3))).astype(int)
+    array([[0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 0, 0, 0, 0]])
+    >>> # Opening can also smooth corners
+    >>> ndimage.binary_opening(a).astype(int)
+    array([[0, 0, 0, 0, 0],
+           [0, 0, 1, 0, 0],
+           [0, 1, 1, 1, 0],
+           [0, 0, 1, 0, 0],
+           [0, 0, 0, 0, 0]])
+    >>> # Opening is the dilation of the erosion of the input
+    >>> ndimage.binary_erosion(a).astype(int)
+    array([[0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0],
+           [0, 0, 1, 0, 0],
+           [0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0]])
+    >>> ndimage.binary_dilation(ndimage.binary_erosion(a)).astype(int)
+    array([[0, 0, 0, 0, 0],
+           [0, 0, 1, 0, 0],
+           [0, 1, 1, 1, 0],
+           [0, 0, 1, 0, 0],
+           [0, 0, 0, 0, 0]])
+
+    """
+    input = np.asarray(input)
+    if structure is None:
+        rank = input.ndim
+        structure = generate_binary_structure(rank, 1)
+
+    tmp = binary_erosion(input, structure, iterations, mask, None,
+                         border_value, origin, brute_force)
+    return binary_dilation(tmp, structure, iterations, mask, output,
+                           border_value, origin, brute_force)
+
+
+def binary_closing(input, structure=None, iterations=1, output=None,
+                   origin=0, mask=None, border_value=0, brute_force=False):
+    """
+    Multidimensional binary closing with the given structuring element.
+
+    The *closing* of an input image by a structuring element is the
+    *erosion* of the *dilation* of the image by the structuring element.
+
+    Parameters
+    ----------
+    input : array_like
+        Binary array_like to be closed. Non-zero (True) elements form
+        the subset to be closed.
+    structure : array_like, optional
+        Structuring element used for the closing. Non-zero elements are
+        considered True. If no structuring element is provided an element
+        is generated with a square connectivity equal to one (i.e., only
+        nearest neighbors are connected to the center, diagonally-connected
+        elements are not considered neighbors).
+    iterations : int, optional
+        The dilation step of the closing, then the erosion step are each
+        repeated `iterations` times (one, by default). If iterations is
+        less than 1, each operations is repeated until the result does
+        not change anymore. Only an integer of iterations is accepted.
+    output : ndarray, optional
+        Array of the same shape as input, into which the output is placed.
+        By default, a new array is created.
+    origin : int or tuple of ints, optional
+        Placement of the filter, by default 0.
+    mask : array_like, optional
+        If a mask is given, only those elements with a True value at
+        the corresponding mask element are modified at each iteration.
+
+        .. versionadded:: 1.1.0
+    border_value : int (cast to 0 or 1), optional
+        Value at the border in the output array.
+
+        .. versionadded:: 1.1.0
+    brute_force : boolean, optional
+        Memory condition: if False, only the pixels whose value was changed in
+        the last iteration are tracked as candidates to be updated in the
+        current iteration; if true al pixels are considered as candidates for
+        update, regardless of what happened in the previous iteration.
+        False by default.
+
+        .. versionadded:: 1.1.0
+
+    Returns
+    -------
+    binary_closing : ndarray of bools
+        Closing of the input by the structuring element.
+
+    See Also
+    --------
+    grey_closing, binary_opening, binary_dilation, binary_erosion,
+    generate_binary_structure
+
+    Notes
+    -----
+    *Closing* [1]_ is a mathematical morphology operation [2]_ that
+    consists in the succession of a dilation and an erosion of the
+    input with the same structuring element. Closing therefore fills
+    holes smaller than the structuring element.
+
+    Together with *opening* (`binary_opening`), closing can be used for
+    noise removal.
+
+    References
+    ----------
+    .. [1] https://en.wikipedia.org/wiki/Closing_%28morphology%29
+    .. [2] https://en.wikipedia.org/wiki/Mathematical_morphology
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.zeros((5,5), dtype=int)
+    >>> a[1:-1, 1:-1] = 1; a[2,2] = 0
+    >>> a
+    array([[0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 0, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 0, 0, 0, 0]])
+    >>> # Closing removes small holes
+    >>> ndimage.binary_closing(a).astype(int)
+    array([[0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 0, 0, 0, 0]])
+    >>> # Closing is the erosion of the dilation of the input
+    >>> ndimage.binary_dilation(a).astype(int)
+    array([[0, 1, 1, 1, 0],
+           [1, 1, 1, 1, 1],
+           [1, 1, 1, 1, 1],
+           [1, 1, 1, 1, 1],
+           [0, 1, 1, 1, 0]])
+    >>> ndimage.binary_erosion(ndimage.binary_dilation(a)).astype(int)
+    array([[0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 0, 0, 0, 0]])
+
+
+    >>> a = np.zeros((7,7), dtype=int)
+    >>> a[1:6, 2:5] = 1; a[1:3,3] = 0
+    >>> a
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 0, 1, 0, 0],
+           [0, 0, 1, 0, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> # In addition to removing holes, closing can also
+    >>> # coarsen boundaries with fine hollows.
+    >>> ndimage.binary_closing(a).astype(int)
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 0, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> ndimage.binary_closing(a, structure=np.ones((2,2))).astype(int)
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+
+    """
+    input = np.asarray(input)
+    if structure is None:
+        rank = input.ndim
+        structure = generate_binary_structure(rank, 1)
+
+    tmp = binary_dilation(input, structure, iterations, mask, None,
+                          border_value, origin, brute_force)
+    return binary_erosion(tmp, structure, iterations, mask, output,
+                          border_value, origin, brute_force)
+
+
+def binary_hit_or_miss(input, structure1=None, structure2=None,
+                       output=None, origin1=0, origin2=None):
+    """
+    Multidimensional binary hit-or-miss transform.
+
+    The hit-or-miss transform finds the locations of a given pattern
+    inside the input image.
+
+    Parameters
+    ----------
+    input : array_like (cast to booleans)
+        Binary image where a pattern is to be detected.
+    structure1 : array_like (cast to booleans), optional
+        Part of the structuring element to be fitted to the foreground
+        (non-zero elements) of `input`. If no value is provided, a
+        structure of square connectivity 1 is chosen.
+    structure2 : array_like (cast to booleans), optional
+        Second part of the structuring element that has to miss completely
+        the foreground. If no value is provided, the complementary of
+        `structure1` is taken.
+    output : ndarray, optional
+        Array of the same shape as input, into which the output is placed.
+        By default, a new array is created.
+    origin1 : int or tuple of ints, optional
+        Placement of the first part of the structuring element `structure1`,
+        by default 0 for a centered structure.
+    origin2 : int or tuple of ints, optional
+        Placement of the second part of the structuring element `structure2`,
+        by default 0 for a centered structure. If a value is provided for
+        `origin1` and not for `origin2`, then `origin2` is set to `origin1`.
+
+    Returns
+    -------
+    binary_hit_or_miss : ndarray
+        Hit-or-miss transform of `input` with the given structuring
+        element (`structure1`, `structure2`).
+
+    See Also
+    --------
+    binary_erosion
+
+    References
+    ----------
+    .. [1] https://en.wikipedia.org/wiki/Hit-or-miss_transform
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.zeros((7,7), dtype=int)
+    >>> a[1, 1] = 1; a[2:4, 2:4] = 1; a[4:6, 4:6] = 1
+    >>> a
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 1, 0, 0, 0, 0, 0],
+           [0, 0, 1, 1, 0, 0, 0],
+           [0, 0, 1, 1, 0, 0, 0],
+           [0, 0, 0, 0, 1, 1, 0],
+           [0, 0, 0, 0, 1, 1, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> structure1 = np.array([[1, 0, 0], [0, 1, 1], [0, 1, 1]])
+    >>> structure1
+    array([[1, 0, 0],
+           [0, 1, 1],
+           [0, 1, 1]])
+    >>> # Find the matches of structure1 in the array a
+    >>> ndimage.binary_hit_or_miss(a, structure1=structure1).astype(int)
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 1, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> # Change the origin of the filter
+    >>> # origin1=1 is equivalent to origin1=(1,1) here
+    >>> ndimage.binary_hit_or_miss(a, structure1=structure1,\\
+    ... origin1=1).astype(int)
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 1, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 1, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+
+    """
+    input = np.asarray(input)
+    if structure1 is None:
+        structure1 = generate_binary_structure(input.ndim, 1)
+    if structure2 is None:
+        structure2 = np.logical_not(structure1)
+    origin1 = _ni_support._normalize_sequence(origin1, input.ndim)
+    if origin2 is None:
+        origin2 = origin1
+    else:
+        origin2 = _ni_support._normalize_sequence(origin2, input.ndim)
+
+    tmp1 = _binary_erosion(input, structure1, 1, None, None, 0, origin1,
+                           0, False)
+    inplace = isinstance(output, np.ndarray)
+    result = _binary_erosion(input, structure2, 1, None, output, 0,
+                             origin2, 1, False)
+    if inplace:
+        np.logical_not(output, output)
+        np.logical_and(tmp1, output, output)
+    else:
+        np.logical_not(result, result)
+        return np.logical_and(tmp1, result)
+
+
+def binary_propagation(input, structure=None, mask=None,
+                       output=None, border_value=0, origin=0):
+    """
+    Multidimensional binary propagation with the given structuring element.
+
+    Parameters
+    ----------
+    input : array_like
+        Binary image to be propagated inside `mask`.
+    structure : array_like, optional
+        Structuring element used in the successive dilations. The output
+        may depend on the structuring element, especially if `mask` has
+        several connex components. If no structuring element is
+        provided, an element is generated with a squared connectivity equal
+        to one.
+    mask : array_like, optional
+        Binary mask defining the region into which `input` is allowed to
+        propagate.
+    output : ndarray, optional
+        Array of the same shape as input, into which the output is placed.
+        By default, a new array is created.
+    border_value : int (cast to 0 or 1), optional
+        Value at the border in the output array.
+    origin : int or tuple of ints, optional
+        Placement of the filter, by default 0.
+
+    Returns
+    -------
+    binary_propagation : ndarray
+        Binary propagation of `input` inside `mask`.
+
+    Notes
+    -----
+    This function is functionally equivalent to calling binary_dilation
+    with the number of iterations less than one: iterative dilation until
+    the result does not change anymore.
+
+    The succession of an erosion and propagation inside the original image
+    can be used instead of an *opening* for deleting small objects while
+    keeping the contours of larger objects untouched.
+
+    References
+    ----------
+    .. [1] http://cmm.ensmp.fr/~serra/cours/pdf/en/ch6en.pdf, slide 15.
+    .. [2] I.T. Young, J.J. Gerbrands, and L.J. van Vliet, "Fundamentals of
+        image processing", 1998
+        ftp://qiftp.tudelft.nl/DIPimage/docs/FIP2.3.pdf
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> input = np.zeros((8, 8), dtype=int)
+    >>> input[2, 2] = 1
+    >>> mask = np.zeros((8, 8), dtype=int)
+    >>> mask[1:4, 1:4] = mask[4, 4]  = mask[6:8, 6:8] = 1
+    >>> input
+    array([[0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0]])
+    >>> mask
+    array([[0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0, 0, 0, 0],
+           [0, 0, 0, 0, 1, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 1, 1],
+           [0, 0, 0, 0, 0, 0, 1, 1]])
+    >>> ndimage.binary_propagation(input, mask=mask).astype(int)
+    array([[0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0]])
+    >>> ndimage.binary_propagation(input, mask=mask,\\
+    ... structure=np.ones((3,3))).astype(int)
+    array([[0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0, 0, 0, 0],
+           [0, 0, 0, 0, 1, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0, 0]])
+
+    >>> # Comparison between opening and erosion+propagation
+    >>> a = np.zeros((6,6), dtype=int)
+    >>> a[2:5, 2:5] = 1; a[0, 0] = 1; a[5, 5] = 1
+    >>> a
+    array([[1, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 1, 1, 0],
+           [0, 0, 1, 1, 1, 0],
+           [0, 0, 1, 1, 1, 0],
+           [0, 0, 0, 0, 0, 1]])
+    >>> ndimage.binary_opening(a).astype(int)
+    array([[0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0],
+           [0, 0, 0, 1, 0, 0],
+           [0, 0, 0, 0, 0, 0]])
+    >>> b = ndimage.binary_erosion(a)
+    >>> b.astype(int)
+    array([[0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 1, 0, 0],
+           [0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0]])
+    >>> ndimage.binary_propagation(b, mask=a).astype(int)
+    array([[0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 1, 1, 0],
+           [0, 0, 1, 1, 1, 0],
+           [0, 0, 1, 1, 1, 0],
+           [0, 0, 0, 0, 0, 0]])
+
+    """
+    return binary_dilation(input, structure, -1, mask, output,
+                           border_value, origin)
+
+
+def binary_fill_holes(input, structure=None, output=None, origin=0):
+    """
+    Fill the holes in binary objects.
+
+
+    Parameters
+    ----------
+    input : array_like
+        N-D binary array with holes to be filled
+    structure : array_like, optional
+        Structuring element used in the computation; large-size elements
+        make computations faster but may miss holes separated from the
+        background by thin regions. The default element (with a square
+        connectivity equal to one) yields the intuitive result where all
+        holes in the input have been filled.
+    output : ndarray, optional
+        Array of the same shape as input, into which the output is placed.
+        By default, a new array is created.
+    origin : int, tuple of ints, optional
+        Position of the structuring element.
+
+    Returns
+    -------
+    out : ndarray
+        Transformation of the initial image `input` where holes have been
+        filled.
+
+    See Also
+    --------
+    binary_dilation, binary_propagation, label
+
+    Notes
+    -----
+    The algorithm used in this function consists in invading the complementary
+    of the shapes in `input` from the outer boundary of the image,
+    using binary dilations. Holes are not connected to the boundary and are
+    therefore not invaded. The result is the complementary subset of the
+    invaded region.
+
+    References
+    ----------
+    .. [1] https://en.wikipedia.org/wiki/Mathematical_morphology
+
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.zeros((5, 5), dtype=int)
+    >>> a[1:4, 1:4] = 1
+    >>> a[2,2] = 0
+    >>> a
+    array([[0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 0, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 0, 0, 0, 0]])
+    >>> ndimage.binary_fill_holes(a).astype(int)
+    array([[0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 0, 0, 0, 0]])
+    >>> # Too big structuring element
+    >>> ndimage.binary_fill_holes(a, structure=np.ones((5,5))).astype(int)
+    array([[0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 0],
+           [0, 1, 0, 1, 0],
+           [0, 1, 1, 1, 0],
+           [0, 0, 0, 0, 0]])
+
+    """
+    mask = np.logical_not(input)
+    tmp = np.zeros(mask.shape, bool)
+    inplace = isinstance(output, np.ndarray)
+    if inplace:
+        binary_dilation(tmp, structure, -1, mask, output, 1, origin)
+        np.logical_not(output, output)
+    else:
+        output = binary_dilation(tmp, structure, -1, mask, None, 1,
+                                 origin)
+        np.logical_not(output, output)
+        return output
+
+
+def grey_erosion(input, size=None, footprint=None, structure=None,
+                 output=None, mode="reflect", cval=0.0, origin=0):
+    """
+    Calculate a greyscale erosion, using either a structuring element,
+    or a footprint corresponding to a flat structuring element.
+
+    Grayscale erosion is a mathematical morphology operation. For the
+    simple case of a full and flat structuring element, it can be viewed
+    as a minimum filter over a sliding window.
+
+    Parameters
+    ----------
+    input : array_like
+        Array over which the grayscale erosion is to be computed.
+    size : tuple of ints
+        Shape of a flat and full structuring element used for the grayscale
+        erosion. Optional if `footprint` or `structure` is provided.
+    footprint : array of ints, optional
+        Positions of non-infinite elements of a flat structuring element
+        used for the grayscale erosion. Non-zero values give the set of
+        neighbors of the center over which the minimum is chosen.
+    structure : array of ints, optional
+        Structuring element used for the grayscale erosion. `structure`
+        may be a non-flat structuring element. The `structure` array applies a
+        subtractive offset for each pixel in the neighborhood.
+    output : array, optional
+        An array used for storing the output of the erosion may be provided.
+    mode : {'reflect','constant','nearest','mirror', 'wrap'}, optional
+        The `mode` parameter determines how the array borders are
+        handled, where `cval` is the value when mode is equal to
+        'constant'. Default is 'reflect'
+    cval : scalar, optional
+        Value to fill past edges of input if `mode` is 'constant'. Default
+        is 0.0.
+    origin : scalar, optional
+        The `origin` parameter controls the placement of the filter.
+        Default 0
+
+    Returns
+    -------
+    output : ndarray
+        Grayscale erosion of `input`.
+
+    See Also
+    --------
+    binary_erosion, grey_dilation, grey_opening, grey_closing
+    generate_binary_structure, minimum_filter
+
+    Notes
+    -----
+    The grayscale erosion of an image input by a structuring element s defined
+    over a domain E is given by:
+
+    (input+s)(x) = min {input(y) - s(x-y), for y in E}
+
+    In particular, for structuring elements defined as
+    s(y) = 0 for y in E, the grayscale erosion computes the minimum of the
+    input image inside a sliding window defined by E.
+
+    Grayscale erosion [1]_ is a *mathematical morphology* operation [2]_.
+
+    References
+    ----------
+    .. [1] https://en.wikipedia.org/wiki/Erosion_%28morphology%29
+    .. [2] https://en.wikipedia.org/wiki/Mathematical_morphology
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.zeros((7,7), dtype=int)
+    >>> a[1:6, 1:6] = 3
+    >>> a[4,4] = 2; a[2,3] = 1
+    >>> a
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 3, 3, 3, 3, 3, 0],
+           [0, 3, 3, 1, 3, 3, 0],
+           [0, 3, 3, 3, 3, 3, 0],
+           [0, 3, 3, 3, 2, 3, 0],
+           [0, 3, 3, 3, 3, 3, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> ndimage.grey_erosion(a, size=(3,3))
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 3, 2, 2, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> footprint = ndimage.generate_binary_structure(2, 1)
+    >>> footprint
+    array([[False,  True, False],
+           [ True,  True,  True],
+           [False,  True, False]], dtype=bool)
+    >>> # Diagonally-connected elements are not considered neighbors
+    >>> ndimage.grey_erosion(a, footprint=footprint)
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 3, 1, 2, 0, 0],
+           [0, 0, 3, 2, 2, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+
+    """
+    if size is None and footprint is None and structure is None:
+        raise ValueError("size, footprint, or structure must be specified")
+
+    return _filters._min_or_max_filter(input, size, footprint, structure,
+                                       output, mode, cval, origin, 1)
+
+
+def grey_dilation(input, size=None, footprint=None, structure=None,
+                  output=None, mode="reflect", cval=0.0, origin=0):
+    """
+    Calculate a greyscale dilation, using either a structuring element,
+    or a footprint corresponding to a flat structuring element.
+
+    Grayscale dilation is a mathematical morphology operation. For the
+    simple case of a full and flat structuring element, it can be viewed
+    as a maximum filter over a sliding window.
+
+    Parameters
+    ----------
+    input : array_like
+        Array over which the grayscale dilation is to be computed.
+    size : tuple of ints
+        Shape of a flat and full structuring element used for the grayscale
+        dilation. Optional if `footprint` or `structure` is provided.
+    footprint : array of ints, optional
+        Positions of non-infinite elements of a flat structuring element
+        used for the grayscale dilation. Non-zero values give the set of
+        neighbors of the center over which the maximum is chosen.
+    structure : array of ints, optional
+        Structuring element used for the grayscale dilation. `structure`
+        may be a non-flat structuring element. The `structure` array applies an
+        additive offset for each pixel in the neighborhood.
+    output : array, optional
+        An array used for storing the output of the dilation may be provided.
+    mode : {'reflect','constant','nearest','mirror', 'wrap'}, optional
+        The `mode` parameter determines how the array borders are
+        handled, where `cval` is the value when mode is equal to
+        'constant'. Default is 'reflect'
+    cval : scalar, optional
+        Value to fill past edges of input if `mode` is 'constant'. Default
+        is 0.0.
+    origin : scalar, optional
+        The `origin` parameter controls the placement of the filter.
+        Default 0
+
+    Returns
+    -------
+    grey_dilation : ndarray
+        Grayscale dilation of `input`.
+
+    See Also
+    --------
+    binary_dilation, grey_erosion, grey_closing, grey_opening
+    generate_binary_structure, maximum_filter
+
+    Notes
+    -----
+    The grayscale dilation of an image input by a structuring element s defined
+    over a domain E is given by:
+
+    (input+s)(x) = max {input(y) + s(x-y), for y in E}
+
+    In particular, for structuring elements defined as
+    s(y) = 0 for y in E, the grayscale dilation computes the maximum of the
+    input image inside a sliding window defined by E.
+
+    Grayscale dilation [1]_ is a *mathematical morphology* operation [2]_.
+
+    References
+    ----------
+    .. [1] https://en.wikipedia.org/wiki/Dilation_%28morphology%29
+    .. [2] https://en.wikipedia.org/wiki/Mathematical_morphology
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.zeros((7,7), dtype=int)
+    >>> a[2:5, 2:5] = 1
+    >>> a[4,4] = 2; a[2,3] = 3
+    >>> a
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 3, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 2, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> ndimage.grey_dilation(a, size=(3,3))
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 1, 3, 3, 3, 1, 0],
+           [0, 1, 3, 3, 3, 1, 0],
+           [0, 1, 3, 3, 3, 2, 0],
+           [0, 1, 1, 2, 2, 2, 0],
+           [0, 1, 1, 2, 2, 2, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> ndimage.grey_dilation(a, footprint=np.ones((3,3)))
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 1, 3, 3, 3, 1, 0],
+           [0, 1, 3, 3, 3, 1, 0],
+           [0, 1, 3, 3, 3, 2, 0],
+           [0, 1, 1, 2, 2, 2, 0],
+           [0, 1, 1, 2, 2, 2, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> s = ndimage.generate_binary_structure(2,1)
+    >>> s
+    array([[False,  True, False],
+           [ True,  True,  True],
+           [False,  True, False]], dtype=bool)
+    >>> ndimage.grey_dilation(a, footprint=s)
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 3, 1, 0, 0],
+           [0, 1, 3, 3, 3, 1, 0],
+           [0, 1, 1, 3, 2, 1, 0],
+           [0, 1, 1, 2, 2, 2, 0],
+           [0, 0, 1, 1, 2, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> ndimage.grey_dilation(a, size=(3,3), structure=np.ones((3,3)))
+    array([[1, 1, 1, 1, 1, 1, 1],
+           [1, 2, 4, 4, 4, 2, 1],
+           [1, 2, 4, 4, 4, 2, 1],
+           [1, 2, 4, 4, 4, 3, 1],
+           [1, 2, 2, 3, 3, 3, 1],
+           [1, 2, 2, 3, 3, 3, 1],
+           [1, 1, 1, 1, 1, 1, 1]])
+
+    """
+    if size is None and footprint is None and structure is None:
+        raise ValueError("size, footprint, or structure must be specified")
+    if structure is not None:
+        structure = np.asarray(structure)
+        structure = structure[tuple([slice(None, None, -1)] *
+                                    structure.ndim)]
+    if footprint is not None:
+        footprint = np.asarray(footprint)
+        footprint = footprint[tuple([slice(None, None, -1)] *
+                                    footprint.ndim)]
+
+    input = np.asarray(input)
+    origin = _ni_support._normalize_sequence(origin, input.ndim)
+    for ii in range(len(origin)):
+        origin[ii] = -origin[ii]
+        if footprint is not None:
+            sz = footprint.shape[ii]
+        elif structure is not None:
+            sz = structure.shape[ii]
+        elif np.isscalar(size):
+            sz = size
+        else:
+            sz = size[ii]
+        if not sz & 1:
+            origin[ii] -= 1
+
+    return _filters._min_or_max_filter(input, size, footprint, structure,
+                                       output, mode, cval, origin, 0)
+
+
+def grey_opening(input, size=None, footprint=None, structure=None,
+                 output=None, mode="reflect", cval=0.0, origin=0):
+    """
+    Multidimensional grayscale opening.
+
+    A grayscale opening consists in the succession of a grayscale erosion,
+    and a grayscale dilation.
+
+    Parameters
+    ----------
+    input : array_like
+        Array over which the grayscale opening is to be computed.
+    size : tuple of ints
+        Shape of a flat and full structuring element used for the grayscale
+        opening. Optional if `footprint` or `structure` is provided.
+    footprint : array of ints, optional
+        Positions of non-infinite elements of a flat structuring element
+        used for the grayscale opening.
+    structure : array of ints, optional
+        Structuring element used for the grayscale opening. `structure`
+        may be a non-flat structuring element. The `structure` array applies
+        offsets to the pixels in a neighborhood (the offset is additive during
+        dilation and subtractive during erosion).
+    output : array, optional
+        An array used for storing the output of the opening may be provided.
+    mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional
+        The `mode` parameter determines how the array borders are
+        handled, where `cval` is the value when mode is equal to
+        'constant'. Default is 'reflect'
+    cval : scalar, optional
+        Value to fill past edges of input if `mode` is 'constant'. Default
+        is 0.0.
+    origin : scalar, optional
+        The `origin` parameter controls the placement of the filter.
+        Default 0
+
+    Returns
+    -------
+    grey_opening : ndarray
+        Result of the grayscale opening of `input` with `structure`.
+
+    See Also
+    --------
+    binary_opening, grey_dilation, grey_erosion, grey_closing
+    generate_binary_structure
+
+    Notes
+    -----
+    The action of a grayscale opening with a flat structuring element amounts
+    to smoothen high local maxima, whereas binary opening erases small objects.
+
+    References
+    ----------
+    .. [1] https://en.wikipedia.org/wiki/Mathematical_morphology
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.arange(36).reshape((6,6))
+    >>> a[3, 3] = 50
+    >>> a
+    array([[ 0,  1,  2,  3,  4,  5],
+           [ 6,  7,  8,  9, 10, 11],
+           [12, 13, 14, 15, 16, 17],
+           [18, 19, 20, 50, 22, 23],
+           [24, 25, 26, 27, 28, 29],
+           [30, 31, 32, 33, 34, 35]])
+    >>> ndimage.grey_opening(a, size=(3,3))
+    array([[ 0,  1,  2,  3,  4,  4],
+           [ 6,  7,  8,  9, 10, 10],
+           [12, 13, 14, 15, 16, 16],
+           [18, 19, 20, 22, 22, 22],
+           [24, 25, 26, 27, 28, 28],
+           [24, 25, 26, 27, 28, 28]])
+    >>> # Note that the local maximum a[3,3] has disappeared
+
+    """
+    if (size is not None) and (footprint is not None):
+        warnings.warn("ignoring size because footprint is set",
+                      UserWarning, stacklevel=2)
+    tmp = grey_erosion(input, size, footprint, structure, None, mode,
+                       cval, origin)
+    return grey_dilation(tmp, size, footprint, structure, output, mode,
+                         cval, origin)
+
+
+def grey_closing(input, size=None, footprint=None, structure=None,
+                 output=None, mode="reflect", cval=0.0, origin=0):
+    """
+    Multidimensional grayscale closing.
+
+    A grayscale closing consists in the succession of a grayscale dilation,
+    and a grayscale erosion.
+
+    Parameters
+    ----------
+    input : array_like
+        Array over which the grayscale closing is to be computed.
+    size : tuple of ints
+        Shape of a flat and full structuring element used for the grayscale
+        closing. Optional if `footprint` or `structure` is provided.
+    footprint : array of ints, optional
+        Positions of non-infinite elements of a flat structuring element
+        used for the grayscale closing.
+    structure : array of ints, optional
+        Structuring element used for the grayscale closing. `structure`
+        may be a non-flat structuring element. The `structure` array applies
+        offsets to the pixels in a neighborhood (the offset is additive during
+        dilation and subtractive during erosion)
+    output : array, optional
+        An array used for storing the output of the closing may be provided.
+    mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional
+        The `mode` parameter determines how the array borders are
+        handled, where `cval` is the value when mode is equal to
+        'constant'. Default is 'reflect'
+    cval : scalar, optional
+        Value to fill past edges of input if `mode` is 'constant'. Default
+        is 0.0.
+    origin : scalar, optional
+        The `origin` parameter controls the placement of the filter.
+        Default 0
+
+    Returns
+    -------
+    grey_closing : ndarray
+        Result of the grayscale closing of `input` with `structure`.
+
+    See Also
+    --------
+    binary_closing, grey_dilation, grey_erosion, grey_opening,
+    generate_binary_structure
+
+    Notes
+    -----
+    The action of a grayscale closing with a flat structuring element amounts
+    to smoothen deep local minima, whereas binary closing fills small holes.
+
+    References
+    ----------
+    .. [1] https://en.wikipedia.org/wiki/Mathematical_morphology
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.arange(36).reshape((6,6))
+    >>> a[3,3] = 0
+    >>> a
+    array([[ 0,  1,  2,  3,  4,  5],
+           [ 6,  7,  8,  9, 10, 11],
+           [12, 13, 14, 15, 16, 17],
+           [18, 19, 20,  0, 22, 23],
+           [24, 25, 26, 27, 28, 29],
+           [30, 31, 32, 33, 34, 35]])
+    >>> ndimage.grey_closing(a, size=(3,3))
+    array([[ 7,  7,  8,  9, 10, 11],
+           [ 7,  7,  8,  9, 10, 11],
+           [13, 13, 14, 15, 16, 17],
+           [19, 19, 20, 20, 22, 23],
+           [25, 25, 26, 27, 28, 29],
+           [31, 31, 32, 33, 34, 35]])
+    >>> # Note that the local minimum a[3,3] has disappeared
+
+    """
+    if (size is not None) and (footprint is not None):
+        warnings.warn("ignoring size because footprint is set",
+                      UserWarning, stacklevel=2)
+    tmp = grey_dilation(input, size, footprint, structure, None, mode,
+                        cval, origin)
+    return grey_erosion(tmp, size, footprint, structure, output, mode,
+                        cval, origin)
+
+
+def morphological_gradient(input, size=None, footprint=None, structure=None,
+                           output=None, mode="reflect", cval=0.0, origin=0):
+    """
+    Multidimensional morphological gradient.
+
+    The morphological gradient is calculated as the difference between a
+    dilation and an erosion of the input with a given structuring element.
+
+    Parameters
+    ----------
+    input : array_like
+        Array over which to compute the morphlogical gradient.
+    size : tuple of ints
+        Shape of a flat and full structuring element used for the mathematical
+        morphology operations. Optional if `footprint` or `structure` is
+        provided. A larger `size` yields a more blurred gradient.
+    footprint : array of ints, optional
+        Positions of non-infinite elements of a flat structuring element
+        used for the morphology operations. Larger footprints
+        give a more blurred morphological gradient.
+    structure : array of ints, optional
+        Structuring element used for the morphology operations. `structure` may
+        be a non-flat structuring element. The `structure` array applies
+        offsets to the pixels in a neighborhood (the offset is additive during
+        dilation and subtractive during erosion)
+    output : array, optional
+        An array used for storing the output of the morphological gradient
+        may be provided.
+    mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional
+        The `mode` parameter determines how the array borders are
+        handled, where `cval` is the value when mode is equal to
+        'constant'. Default is 'reflect'
+    cval : scalar, optional
+        Value to fill past edges of input if `mode` is 'constant'. Default
+        is 0.0.
+    origin : scalar, optional
+        The `origin` parameter controls the placement of the filter.
+        Default 0
+
+    Returns
+    -------
+    morphological_gradient : ndarray
+        Morphological gradient of `input`.
+
+    See Also
+    --------
+    grey_dilation, grey_erosion, gaussian_gradient_magnitude
+
+    Notes
+    -----
+    For a flat structuring element, the morphological gradient
+    computed at a given point corresponds to the maximal difference
+    between elements of the input among the elements covered by the
+    structuring element centered on the point.
+
+    References
+    ----------
+    .. [1] https://en.wikipedia.org/wiki/Mathematical_morphology
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.zeros((7,7), dtype=int)
+    >>> a[2:5, 2:5] = 1
+    >>> ndimage.morphological_gradient(a, size=(3,3))
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 1, 1, 0],
+           [0, 1, 1, 1, 1, 1, 0],
+           [0, 1, 1, 0, 1, 1, 0],
+           [0, 1, 1, 1, 1, 1, 0],
+           [0, 1, 1, 1, 1, 1, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> # The morphological gradient is computed as the difference
+    >>> # between a dilation and an erosion
+    >>> ndimage.grey_dilation(a, size=(3,3)) -\\
+    ...  ndimage.grey_erosion(a, size=(3,3))
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 1, 1, 1, 1, 1, 0],
+           [0, 1, 1, 1, 1, 1, 0],
+           [0, 1, 1, 0, 1, 1, 0],
+           [0, 1, 1, 1, 1, 1, 0],
+           [0, 1, 1, 1, 1, 1, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> a = np.zeros((7,7), dtype=int)
+    >>> a[2:5, 2:5] = 1
+    >>> a[4,4] = 2; a[2,3] = 3
+    >>> a
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 1, 3, 1, 0, 0],
+           [0, 0, 1, 1, 1, 0, 0],
+           [0, 0, 1, 1, 2, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+    >>> ndimage.morphological_gradient(a, size=(3,3))
+    array([[0, 0, 0, 0, 0, 0, 0],
+           [0, 1, 3, 3, 3, 1, 0],
+           [0, 1, 3, 3, 3, 1, 0],
+           [0, 1, 3, 2, 3, 2, 0],
+           [0, 1, 1, 2, 2, 2, 0],
+           [0, 1, 1, 2, 2, 2, 0],
+           [0, 0, 0, 0, 0, 0, 0]])
+
+    """
+    tmp = grey_dilation(input, size, footprint, structure, None, mode,
+                        cval, origin)
+    if isinstance(output, np.ndarray):
+        grey_erosion(input, size, footprint, structure, output, mode,
+                     cval, origin)
+        return np.subtract(tmp, output, output)
+    else:
+        return (tmp - grey_erosion(input, size, footprint, structure,
+                                   None, mode, cval, origin))
+
+
+def morphological_laplace(input, size=None, footprint=None,
+                          structure=None, output=None,
+                          mode="reflect", cval=0.0, origin=0):
+    """
+    Multidimensional morphological laplace.
+
+    Parameters
+    ----------
+    input : array_like
+        Input.
+    size : tuple of ints
+        Shape of a flat and full structuring element used for the mathematical
+        morphology operations. Optional if `footprint` or `structure` is
+        provided.
+    footprint : array of ints, optional
+        Positions of non-infinite elements of a flat structuring element
+        used for the morphology operations.
+    structure : array of ints, optional
+        Structuring element used for the morphology operations. `structure` may
+        be a non-flat structuring element. The `structure` array applies
+        offsets to the pixels in a neighborhood (the offset is additive during
+        dilation and subtractive during erosion)
+    output : ndarray, optional
+        An output array can optionally be provided.
+    mode : {'reflect','constant','nearest','mirror', 'wrap'}, optional
+        The mode parameter determines how the array borders are handled.
+        For 'constant' mode, values beyond borders are set to be `cval`.
+        Default is 'reflect'.
+    cval : scalar, optional
+        Value to fill past edges of input if mode is 'constant'.
+        Default is 0.0
+    origin : origin, optional
+        The origin parameter controls the placement of the filter.
+
+    Returns
+    -------
+    morphological_laplace : ndarray
+        Output
+
+    """
+    tmp1 = grey_dilation(input, size, footprint, structure, None, mode,
+                         cval, origin)
+    if isinstance(output, np.ndarray):
+        grey_erosion(input, size, footprint, structure, output, mode,
+                     cval, origin)
+        np.add(tmp1, output, output)
+        np.subtract(output, input, output)
+        return np.subtract(output, input, output)
+    else:
+        tmp2 = grey_erosion(input, size, footprint, structure, None, mode,
+                            cval, origin)
+        np.add(tmp1, tmp2, tmp2)
+        np.subtract(tmp2, input, tmp2)
+        np.subtract(tmp2, input, tmp2)
+        return tmp2
+
+
+def white_tophat(input, size=None, footprint=None, structure=None,
+                 output=None, mode="reflect", cval=0.0, origin=0):
+    """
+    Multidimensional white tophat filter.
+
+    Parameters
+    ----------
+    input : array_like
+        Input.
+    size : tuple of ints
+        Shape of a flat and full structuring element used for the filter.
+        Optional if `footprint` or `structure` is provided.
+    footprint : array of ints, optional
+        Positions of elements of a flat structuring element
+        used for the white tophat filter.
+    structure : array of ints, optional
+        Structuring element used for the filter. `structure` may be a non-flat
+        structuring element. The `structure` array applies offsets to the
+        pixels in a neighborhood (the offset is additive during dilation and
+        subtractive during erosion)
+    output : array, optional
+        An array used for storing the output of the filter may be provided.
+    mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional
+        The `mode` parameter determines how the array borders are
+        handled, where `cval` is the value when mode is equal to
+        'constant'. Default is 'reflect'
+    cval : scalar, optional
+        Value to fill past edges of input if `mode` is 'constant'.
+        Default is 0.0.
+    origin : scalar, optional
+        The `origin` parameter controls the placement of the filter.
+        Default is 0.
+
+    Returns
+    -------
+    output : ndarray
+        Result of the filter of `input` with `structure`.
+
+    See Also
+    --------
+    black_tophat
+
+    Examples
+    --------
+    Subtract gray background from a bright peak.
+
+    >>> from scipy.ndimage import generate_binary_structure, white_tophat
+    >>> import numpy as np
+    >>> square = generate_binary_structure(rank=2, connectivity=3)
+    >>> bright_on_gray = np.array([[2, 3, 3, 3, 2],
+    ...                            [3, 4, 5, 4, 3],
+    ...                            [3, 5, 9, 5, 3],
+    ...                            [3, 4, 5, 4, 3],
+    ...                            [2, 3, 3, 3, 2]])
+    >>> white_tophat(input=bright_on_gray, structure=square)
+    array([[0, 0, 0, 0, 0],
+           [0, 0, 1, 0, 0],
+           [0, 1, 5, 1, 0],
+           [0, 0, 1, 0, 0],
+           [0, 0, 0, 0, 0]])
+
+    """
+    if (size is not None) and (footprint is not None):
+        warnings.warn("ignoring size because footprint is set",
+                      UserWarning, stacklevel=2)
+    tmp = grey_erosion(input, size, footprint, structure, None, mode,
+                       cval, origin)
+    tmp = grey_dilation(tmp, size, footprint, structure, output, mode,
+                        cval, origin)
+    if tmp is None:
+        tmp = output
+
+    if input.dtype == np.bool_ and tmp.dtype == np.bool_:
+        np.bitwise_xor(input, tmp, out=tmp)
+    else:
+        np.subtract(input, tmp, out=tmp)
+    return tmp
+
+
+def black_tophat(input, size=None, footprint=None,
+                 structure=None, output=None, mode="reflect",
+                 cval=0.0, origin=0):
+    """
+    Multidimensional black tophat filter.
+
+    Parameters
+    ----------
+    input : array_like
+        Input.
+    size : tuple of ints, optional
+        Shape of a flat and full structuring element used for the filter.
+        Optional if `footprint` or `structure` is provided.
+    footprint : array of ints, optional
+        Positions of non-infinite elements of a flat structuring element
+        used for the black tophat filter.
+    structure : array of ints, optional
+        Structuring element used for the filter. `structure` may be a non-flat
+        structuring element. The `structure` array applies offsets to the
+        pixels in a neighborhood (the offset is additive during dilation and
+        subtractive during erosion)
+    output : array, optional
+        An array used for storing the output of the filter may be provided.
+    mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional
+        The `mode` parameter determines how the array borders are
+        handled, where `cval` is the value when mode is equal to
+        'constant'. Default is 'reflect'
+    cval : scalar, optional
+        Value to fill past edges of input if `mode` is 'constant'. Default
+        is 0.0.
+    origin : scalar, optional
+        The `origin` parameter controls the placement of the filter.
+        Default 0
+
+    Returns
+    -------
+    black_tophat : ndarray
+        Result of the filter of `input` with `structure`.
+
+    See Also
+    --------
+    white_tophat, grey_opening, grey_closing
+
+    Examples
+    --------
+    Change dark peak to bright peak and subtract background.
+
+    >>> from scipy.ndimage import generate_binary_structure, black_tophat
+    >>> import numpy as np
+    >>> square = generate_binary_structure(rank=2, connectivity=3)
+    >>> dark_on_gray = np.array([[7, 6, 6, 6, 7],
+    ...                          [6, 5, 4, 5, 6],
+    ...                          [6, 4, 0, 4, 6],
+    ...                          [6, 5, 4, 5, 6],
+    ...                          [7, 6, 6, 6, 7]])
+    >>> black_tophat(input=dark_on_gray, structure=square)
+    array([[0, 0, 0, 0, 0],
+           [0, 0, 1, 0, 0],
+           [0, 1, 5, 1, 0],
+           [0, 0, 1, 0, 0],
+           [0, 0, 0, 0, 0]])
+
+    """
+    if (size is not None) and (footprint is not None):
+        warnings.warn("ignoring size because footprint is set",
+                      UserWarning, stacklevel=2)
+    tmp = grey_dilation(input, size, footprint, structure, None, mode,
+                        cval, origin)
+    tmp = grey_erosion(tmp, size, footprint, structure, output, mode,
+                       cval, origin)
+    if tmp is None:
+        tmp = output
+
+    if input.dtype == np.bool_ and tmp.dtype == np.bool_:
+        np.bitwise_xor(tmp, input, out=tmp)
+    else:
+        np.subtract(tmp, input, out=tmp)
+    return tmp
+
+
+def distance_transform_bf(input, metric="euclidean", sampling=None,
+                          return_distances=True, return_indices=False,
+                          distances=None, indices=None):
+    """
+    Distance transform function by a brute force algorithm.
+
+    This function calculates the distance transform of the `input`, by
+    replacing each foreground (non-zero) element, with its
+    shortest distance to the background (any zero-valued element).
+
+    In addition to the distance transform, the feature transform can
+    be calculated. In this case the index of the closest background
+    element to each foreground element is returned in a separate array.
+
+    Parameters
+    ----------
+    input : array_like
+        Input
+    metric : {'euclidean', 'taxicab', 'chessboard'}, optional
+        'cityblock' and 'manhattan' are also valid, and map to 'taxicab'.
+        The default is 'euclidean'.
+    sampling : float, or sequence of float, optional
+        This parameter is only used when `metric` is 'euclidean'.
+        Spacing of elements along each dimension. If a sequence, must be of
+        length equal to the input rank; if a single number, this is used for
+        all axes. If not specified, a grid spacing of unity is implied.
+    return_distances : bool, optional
+        Whether to calculate the distance transform.
+        Default is True.
+    return_indices : bool, optional
+        Whether to calculate the feature transform.
+        Default is False.
+    distances : ndarray, optional
+        An output array to store the calculated distance transform, instead of
+        returning it.
+        `return_distances` must be True.
+        It must be the same shape as `input`, and of type float64 if `metric`
+        is 'euclidean', uint32 otherwise.
+    indices : int32 ndarray, optional
+        An output array to store the calculated feature transform, instead of
+        returning it.
+        `return_indicies` must be True.
+        Its shape must be `(input.ndim,) + input.shape`.
+
+    Returns
+    -------
+    distances : ndarray, optional
+        The calculated distance transform. Returned only when
+        `return_distances` is True and `distances` is not supplied.
+        It will have the same shape as the input array.
+    indices : int32 ndarray, optional
+        The calculated feature transform. It has an input-shaped array for each
+        dimension of the input. See distance_transform_edt documentation for an
+        example.
+        Returned only when `return_indices` is True and `indices` is not
+        supplied.
+
+    See Also
+    --------
+    distance_transform_cdt : Faster distance transform for taxicab and
+                             chessboard metrics
+    distance_transform_edt : Faster distance transform for euclidean metric
+
+    Notes
+    -----
+    This function employs a slow brute force algorithm. See also the
+    function `distance_transform_cdt` for more efficient taxicab [1]_ and
+    chessboard algorithms [2]_.
+
+    References
+    ----------
+    .. [1] Taxicab distance. Wikipedia, 2023.
+           https://en.wikipedia.org/wiki/Taxicab_geometry
+    .. [2] Chessboard distance. Wikipedia, 2023.
+           https://en.wikipedia.org/wiki/Chebyshev_distance
+
+    Examples
+    --------
+    Import the necessary modules.
+
+    >>> import numpy as np
+    >>> from scipy.ndimage import distance_transform_bf
+    >>> import matplotlib.pyplot as plt
+    >>> from mpl_toolkits.axes_grid1 import ImageGrid
+
+    First, we create a toy binary image.
+
+    >>> def add_circle(center_x, center_y, radius, image, fillvalue=1):
+    ...     # fill circular area with 1
+    ...     xx, yy = np.mgrid[:image.shape[0], :image.shape[1]]
+    ...     circle = (xx - center_x) ** 2 + (yy - center_y) ** 2
+    ...     circle_shape = np.sqrt(circle) < radius
+    ...     image[circle_shape] = fillvalue
+    ...     return image
+    >>> image = np.zeros((100, 100), dtype=np.uint8)
+    >>> image[35:65, 20:80] = 1
+    >>> image = add_circle(28, 65, 10, image)
+    >>> image = add_circle(37, 30, 10, image)
+    >>> image = add_circle(70, 45, 20, image)
+    >>> image = add_circle(45, 80, 10, image)
+
+    Next, we set up the figure.
+
+    >>> fig = plt.figure(figsize=(8, 8))  # set up the figure structure
+    >>> grid = ImageGrid(fig, 111, nrows_ncols=(2, 2), axes_pad=(0.4, 0.3),
+    ...                  label_mode="1", share_all=True,
+    ...                  cbar_location="right", cbar_mode="each",
+    ...                  cbar_size="7%", cbar_pad="2%")
+    >>> for ax in grid:
+    ...     ax.axis('off')  # remove axes from images
+
+    The top left image is the original binary image.
+
+    >>> binary_image = grid[0].imshow(image, cmap='gray')
+    >>> cbar_binary_image = grid.cbar_axes[0].colorbar(binary_image)
+    >>> cbar_binary_image.set_ticks([0, 1])
+    >>> grid[0].set_title("Binary image: foreground in white")
+
+    The distance transform calculates the distance between foreground pixels
+    and the image background according to a distance metric. Available metrics
+    in `distance_transform_bf` are: ``euclidean`` (default), ``taxicab``
+    and ``chessboard``. The top right image contains the distance transform
+    based on the ``euclidean`` metric.
+
+    >>> distance_transform_euclidean = distance_transform_bf(image)
+    >>> euclidean_transform = grid[1].imshow(distance_transform_euclidean,
+    ...                                      cmap='gray')
+    >>> cbar_euclidean = grid.cbar_axes[1].colorbar(euclidean_transform)
+    >>> colorbar_ticks = [0, 10, 20]
+    >>> cbar_euclidean.set_ticks(colorbar_ticks)
+    >>> grid[1].set_title("Euclidean distance")
+
+    The lower left image contains the distance transform using the ``taxicab``
+    metric.
+
+    >>> distance_transform_taxicab = distance_transform_bf(image,
+    ...                                                    metric='taxicab')
+    >>> taxicab_transformation = grid[2].imshow(distance_transform_taxicab,
+    ...                                         cmap='gray')
+    >>> cbar_taxicab = grid.cbar_axes[2].colorbar(taxicab_transformation)
+    >>> cbar_taxicab.set_ticks(colorbar_ticks)
+    >>> grid[2].set_title("Taxicab distance")
+
+    Finally, the lower right image contains the distance transform using the
+    ``chessboard`` metric.
+
+    >>> distance_transform_cb = distance_transform_bf(image,
+    ...                                               metric='chessboard')
+    >>> chessboard_transformation = grid[3].imshow(distance_transform_cb,
+    ...                                            cmap='gray')
+    >>> cbar_taxicab = grid.cbar_axes[3].colorbar(chessboard_transformation)
+    >>> cbar_taxicab.set_ticks(colorbar_ticks)
+    >>> grid[3].set_title("Chessboard distance")
+    >>> plt.show()
+
+    """
+    ft_inplace = isinstance(indices, np.ndarray)
+    dt_inplace = isinstance(distances, np.ndarray)
+    _distance_tranform_arg_check(
+        dt_inplace, ft_inplace, return_distances, return_indices
+    )
+
+    tmp1 = np.asarray(input) != 0
+    struct = generate_binary_structure(tmp1.ndim, tmp1.ndim)
+    tmp2 = binary_dilation(tmp1, struct)
+    tmp2 = np.logical_xor(tmp1, tmp2)
+    tmp1 = tmp1.astype(np.int8) - tmp2.astype(np.int8)
+    metric = metric.lower()
+    if metric == 'euclidean':
+        metric = 1
+    elif metric in ['taxicab', 'cityblock', 'manhattan']:
+        metric = 2
+    elif metric == 'chessboard':
+        metric = 3
+    else:
+        raise RuntimeError('distance metric not supported')
+    if sampling is not None:
+        sampling = _ni_support._normalize_sequence(sampling, tmp1.ndim)
+        sampling = np.asarray(sampling, dtype=np.float64)
+        if not sampling.flags.contiguous:
+            sampling = sampling.copy()
+    if return_indices:
+        ft = np.zeros(tmp1.shape, dtype=np.int32)
+    else:
+        ft = None
+    if return_distances:
+        if distances is None:
+            if metric == 1:
+                dt = np.zeros(tmp1.shape, dtype=np.float64)
+            else:
+                dt = np.zeros(tmp1.shape, dtype=np.uint32)
+        else:
+            if distances.shape != tmp1.shape:
+                raise RuntimeError('distances array has wrong shape')
+            if metric == 1:
+                if distances.dtype.type != np.float64:
+                    raise RuntimeError('distances array must be float64')
+            else:
+                if distances.dtype.type != np.uint32:
+                    raise RuntimeError('distances array must be uint32')
+            dt = distances
+    else:
+        dt = None
+
+    _nd_image.distance_transform_bf(tmp1, metric, sampling, dt, ft)
+    if return_indices:
+        if isinstance(indices, np.ndarray):
+            if indices.dtype.type != np.int32:
+                raise RuntimeError('indices array must be int32')
+            if indices.shape != (tmp1.ndim,) + tmp1.shape:
+                raise RuntimeError('indices array has wrong shape')
+            tmp2 = indices
+        else:
+            tmp2 = np.indices(tmp1.shape, dtype=np.int32)
+        ft = np.ravel(ft)
+        for ii in range(tmp2.shape[0]):
+            rtmp = np.ravel(tmp2[ii, ...])[ft]
+            rtmp.shape = tmp1.shape
+            tmp2[ii, ...] = rtmp
+        ft = tmp2
+
+    # construct and return the result
+    result = []
+    if return_distances and not dt_inplace:
+        result.append(dt)
+    if return_indices and not ft_inplace:
+        result.append(ft)
+
+    if len(result) == 2:
+        return tuple(result)
+    elif len(result) == 1:
+        return result[0]
+    else:
+        return None
+
+
+def distance_transform_cdt(input, metric='chessboard', return_distances=True,
+                           return_indices=False, distances=None, indices=None):
+    """
+    Distance transform for chamfer type of transforms.
+
+    This function calculates the distance transform of the `input`, by
+    replacing each foreground (non-zero) element, with its
+    shortest distance to the background (any zero-valued element).
+
+    In addition to the distance transform, the feature transform can
+    be calculated. In this case the index of the closest background
+    element to each foreground element is returned in a separate array.
+
+    Parameters
+    ----------
+    input : array_like
+        Input. Values of 0 are treated as background.
+    metric : {'chessboard', 'taxicab'} or array_like, optional
+        The `metric` determines the type of chamfering that is done. If the
+        `metric` is equal to 'taxicab' a structure is generated using
+        `generate_binary_structure` with a squared distance equal to 1. If
+        the `metric` is equal to 'chessboard', a `metric` is generated
+        using `generate_binary_structure` with a squared distance equal to
+        the dimensionality of the array. These choices correspond to the
+        common interpretations of the 'taxicab' and the 'chessboard'
+        distance metrics in two dimensions.
+        A custom metric may be provided, in the form of a matrix where
+        each dimension has a length of three.
+        'cityblock' and 'manhattan' are also valid, and map to 'taxicab'.
+        The default is 'chessboard'.
+    return_distances : bool, optional
+        Whether to calculate the distance transform.
+        Default is True.
+    return_indices : bool, optional
+        Whether to calculate the feature transform.
+        Default is False.
+    distances : int32 ndarray, optional
+        An output array to store the calculated distance transform, instead of
+        returning it.
+        `return_distances` must be True.
+        It must be the same shape as `input`.
+    indices : int32 ndarray, optional
+        An output array to store the calculated feature transform, instead of
+        returning it.
+        `return_indicies` must be True.
+        Its shape must be `(input.ndim,) + input.shape`.
+
+    Returns
+    -------
+    distances : int32 ndarray, optional
+        The calculated distance transform. Returned only when
+        `return_distances` is True, and `distances` is not supplied.
+        It will have the same shape as the input array.
+    indices : int32 ndarray, optional
+        The calculated feature transform. It has an input-shaped array for each
+        dimension of the input. See distance_transform_edt documentation for an
+        example.
+        Returned only when `return_indices` is True, and `indices` is not
+        supplied.
+
+    See Also
+    --------
+    distance_transform_edt : Fast distance transform for euclidean metric
+    distance_transform_bf : Distance transform for different metrics using
+                            a slower brute force algorithm
+
+    Examples
+    --------
+    Import the necessary modules.
+
+    >>> import numpy as np
+    >>> from scipy.ndimage import distance_transform_cdt
+    >>> import matplotlib.pyplot as plt
+    >>> from mpl_toolkits.axes_grid1 import ImageGrid
+
+    First, we create a toy binary image.
+
+    >>> def add_circle(center_x, center_y, radius, image, fillvalue=1):
+    ...     # fill circular area with 1
+    ...     xx, yy = np.mgrid[:image.shape[0], :image.shape[1]]
+    ...     circle = (xx - center_x) ** 2 + (yy - center_y) ** 2
+    ...     circle_shape = np.sqrt(circle) < radius
+    ...     image[circle_shape] = fillvalue
+    ...     return image
+    >>> image = np.zeros((100, 100), dtype=np.uint8)
+    >>> image[35:65, 20:80] = 1
+    >>> image = add_circle(28, 65, 10, image)
+    >>> image = add_circle(37, 30, 10, image)
+    >>> image = add_circle(70, 45, 20, image)
+    >>> image = add_circle(45, 80, 10, image)
+
+    Next, we set up the figure.
+
+    >>> fig = plt.figure(figsize=(5, 15))
+    >>> grid = ImageGrid(fig, 111, nrows_ncols=(3, 1), axes_pad=(0.5, 0.3),
+    ...                  label_mode="1", share_all=True,
+    ...                  cbar_location="right", cbar_mode="each",
+    ...                  cbar_size="7%", cbar_pad="2%")
+    >>> for ax in grid:
+    ...     ax.axis('off')
+    >>> top, middle, bottom = grid
+    >>> colorbar_ticks = [0, 10, 20]
+
+    The top image contains the original binary image.
+
+    >>> binary_image = top.imshow(image, cmap='gray')
+    >>> cbar_binary_image = top.cax.colorbar(binary_image)
+    >>> cbar_binary_image.set_ticks([0, 1])
+    >>> top.set_title("Binary image: foreground in white")
+
+    The middle image contains the distance transform using the ``taxicab``
+    metric.
+
+    >>> distance_taxicab = distance_transform_cdt(image, metric="taxicab")
+    >>> taxicab_transform = middle.imshow(distance_taxicab, cmap='gray')
+    >>> cbar_taxicab = middle.cax.colorbar(taxicab_transform)
+    >>> cbar_taxicab.set_ticks(colorbar_ticks)
+    >>> middle.set_title("Taxicab metric")
+
+    The bottom image contains the distance transform using the ``chessboard``
+    metric.
+
+    >>> distance_chessboard = distance_transform_cdt(image,
+    ...                                              metric="chessboard")
+    >>> chessboard_transform = bottom.imshow(distance_chessboard, cmap='gray')
+    >>> cbar_chessboard = bottom.cax.colorbar(chessboard_transform)
+    >>> cbar_chessboard.set_ticks(colorbar_ticks)
+    >>> bottom.set_title("Chessboard metric")
+    >>> plt.tight_layout()
+    >>> plt.show()
+
+    """
+    ft_inplace = isinstance(indices, np.ndarray)
+    dt_inplace = isinstance(distances, np.ndarray)
+    _distance_tranform_arg_check(
+        dt_inplace, ft_inplace, return_distances, return_indices
+    )
+    input = np.asarray(input)
+    if isinstance(metric, str):
+        if metric in ['taxicab', 'cityblock', 'manhattan']:
+            rank = input.ndim
+            metric = generate_binary_structure(rank, 1)
+        elif metric == 'chessboard':
+            rank = input.ndim
+            metric = generate_binary_structure(rank, rank)
+        else:
+            raise ValueError('invalid metric provided')
+    else:
+        try:
+            metric = np.asarray(metric)
+        except Exception as e:
+            raise ValueError('invalid metric provided') from e
+        for s in metric.shape:
+            if s != 3:
+                raise ValueError('metric sizes must be equal to 3')
+
+    if not metric.flags.contiguous:
+        metric = metric.copy()
+    if dt_inplace:
+        if distances.dtype.type != np.int32:
+            raise ValueError('distances must be of int32 type')
+        if distances.shape != input.shape:
+            raise ValueError('distances has wrong shape')
+        dt = distances
+        dt[...] = np.where(input, -1, 0).astype(np.int32)
+    else:
+        dt = np.where(input, -1, 0).astype(np.int32)
+
+    rank = dt.ndim
+    if return_indices:
+        ft = np.arange(dt.size, dtype=np.int32)
+        ft.shape = dt.shape
+    else:
+        ft = None
+
+    _nd_image.distance_transform_op(metric, dt, ft)
+    dt = dt[tuple([slice(None, None, -1)] * rank)]
+    if return_indices:
+        ft = ft[tuple([slice(None, None, -1)] * rank)]
+    _nd_image.distance_transform_op(metric, dt, ft)
+    dt = dt[tuple([slice(None, None, -1)] * rank)]
+    if return_indices:
+        ft = ft[tuple([slice(None, None, -1)] * rank)]
+        ft = np.ravel(ft)
+        if ft_inplace:
+            if indices.dtype.type != np.int32:
+                raise ValueError('indices array must be int32')
+            if indices.shape != (dt.ndim,) + dt.shape:
+                raise ValueError('indices array has wrong shape')
+            tmp = indices
+        else:
+            tmp = np.indices(dt.shape, dtype=np.int32)
+        for ii in range(tmp.shape[0]):
+            rtmp = np.ravel(tmp[ii, ...])[ft]
+            rtmp.shape = dt.shape
+            tmp[ii, ...] = rtmp
+        ft = tmp
+
+    # construct and return the result
+    result = []
+    if return_distances and not dt_inplace:
+        result.append(dt)
+    if return_indices and not ft_inplace:
+        result.append(ft)
+
+    if len(result) == 2:
+        return tuple(result)
+    elif len(result) == 1:
+        return result[0]
+    else:
+        return None
+
+
+def distance_transform_edt(input, sampling=None, return_distances=True,
+                           return_indices=False, distances=None, indices=None):
+    """
+    Exact Euclidean distance transform.
+
+    This function calculates the distance transform of the `input`, by
+    replacing each foreground (non-zero) element, with its
+    shortest distance to the background (any zero-valued element).
+
+    In addition to the distance transform, the feature transform can
+    be calculated. In this case the index of the closest background
+    element to each foreground element is returned in a separate array.
+
+    Parameters
+    ----------
+    input : array_like
+        Input data to transform. Can be any type but will be converted
+        into binary: 1 wherever input equates to True, 0 elsewhere.
+    sampling : float, or sequence of float, optional
+        Spacing of elements along each dimension. If a sequence, must be of
+        length equal to the input rank; if a single number, this is used for
+        all axes. If not specified, a grid spacing of unity is implied.
+    return_distances : bool, optional
+        Whether to calculate the distance transform.
+        Default is True.
+    return_indices : bool, optional
+        Whether to calculate the feature transform.
+        Default is False.
+    distances : float64 ndarray, optional
+        An output array to store the calculated distance transform, instead of
+        returning it.
+        `return_distances` must be True.
+        It must be the same shape as `input`.
+    indices : int32 ndarray, optional
+        An output array to store the calculated feature transform, instead of
+        returning it.
+        `return_indicies` must be True.
+        Its shape must be `(input.ndim,) + input.shape`.
+
+    Returns
+    -------
+    distances : float64 ndarray, optional
+        The calculated distance transform. Returned only when
+        `return_distances` is True and `distances` is not supplied.
+        It will have the same shape as the input array.
+    indices : int32 ndarray, optional
+        The calculated feature transform. It has an input-shaped array for each
+        dimension of the input. See example below.
+        Returned only when `return_indices` is True and `indices` is not
+        supplied.
+
+    Notes
+    -----
+    The Euclidean distance transform gives values of the Euclidean
+    distance::
+
+                    n
+      y_i = sqrt(sum (x[i]-b[i])**2)
+                    i
+
+    where b[i] is the background point (value 0) with the smallest
+    Euclidean distance to input points x[i], and n is the
+    number of dimensions.
+
+    Examples
+    --------
+    >>> from scipy import ndimage
+    >>> import numpy as np
+    >>> a = np.array(([0,1,1,1,1],
+    ...               [0,0,1,1,1],
+    ...               [0,1,1,1,1],
+    ...               [0,1,1,1,0],
+    ...               [0,1,1,0,0]))
+    >>> ndimage.distance_transform_edt(a)
+    array([[ 0.    ,  1.    ,  1.4142,  2.2361,  3.    ],
+           [ 0.    ,  0.    ,  1.    ,  2.    ,  2.    ],
+           [ 0.    ,  1.    ,  1.4142,  1.4142,  1.    ],
+           [ 0.    ,  1.    ,  1.4142,  1.    ,  0.    ],
+           [ 0.    ,  1.    ,  1.    ,  0.    ,  0.    ]])
+
+    With a sampling of 2 units along x, 1 along y:
+
+    >>> ndimage.distance_transform_edt(a, sampling=[2,1])
+    array([[ 0.    ,  1.    ,  2.    ,  2.8284,  3.6056],
+           [ 0.    ,  0.    ,  1.    ,  2.    ,  3.    ],
+           [ 0.    ,  1.    ,  2.    ,  2.2361,  2.    ],
+           [ 0.    ,  1.    ,  2.    ,  1.    ,  0.    ],
+           [ 0.    ,  1.    ,  1.    ,  0.    ,  0.    ]])
+
+    Asking for indices as well:
+
+    >>> edt, inds = ndimage.distance_transform_edt(a, return_indices=True)
+    >>> inds
+    array([[[0, 0, 1, 1, 3],
+            [1, 1, 1, 1, 3],
+            [2, 2, 1, 3, 3],
+            [3, 3, 4, 4, 3],
+            [4, 4, 4, 4, 4]],
+           [[0, 0, 1, 1, 4],
+            [0, 1, 1, 1, 4],
+            [0, 0, 1, 4, 4],
+            [0, 0, 3, 3, 4],
+            [0, 0, 3, 3, 4]]])
+
+    With arrays provided for inplace outputs:
+
+    >>> indices = np.zeros(((np.ndim(a),) + a.shape), dtype=np.int32)
+    >>> ndimage.distance_transform_edt(a, return_indices=True, indices=indices)
+    array([[ 0.    ,  1.    ,  1.4142,  2.2361,  3.    ],
+           [ 0.    ,  0.    ,  1.    ,  2.    ,  2.    ],
+           [ 0.    ,  1.    ,  1.4142,  1.4142,  1.    ],
+           [ 0.    ,  1.    ,  1.4142,  1.    ,  0.    ],
+           [ 0.    ,  1.    ,  1.    ,  0.    ,  0.    ]])
+    >>> indices
+    array([[[0, 0, 1, 1, 3],
+            [1, 1, 1, 1, 3],
+            [2, 2, 1, 3, 3],
+            [3, 3, 4, 4, 3],
+            [4, 4, 4, 4, 4]],
+           [[0, 0, 1, 1, 4],
+            [0, 1, 1, 1, 4],
+            [0, 0, 1, 4, 4],
+            [0, 0, 3, 3, 4],
+            [0, 0, 3, 3, 4]]])
+
+    """
+    ft_inplace = isinstance(indices, np.ndarray)
+    dt_inplace = isinstance(distances, np.ndarray)
+    _distance_tranform_arg_check(
+        dt_inplace, ft_inplace, return_distances, return_indices
+    )
+
+    # calculate the feature transform
+    input = np.atleast_1d(np.where(input, 1, 0).astype(np.int8))
+    if sampling is not None:
+        sampling = _ni_support._normalize_sequence(sampling, input.ndim)
+        sampling = np.asarray(sampling, dtype=np.float64)
+        if not sampling.flags.contiguous:
+            sampling = sampling.copy()
+
+    if ft_inplace:
+        ft = indices
+        if ft.shape != (input.ndim,) + input.shape:
+            raise RuntimeError('indices array has wrong shape')
+        if ft.dtype.type != np.int32:
+            raise RuntimeError('indices array must be int32')
+    else:
+        ft = np.zeros((input.ndim,) + input.shape, dtype=np.int32)
+
+    _nd_image.euclidean_feature_transform(input, sampling, ft)
+    # if requested, calculate the distance transform
+    if return_distances:
+        dt = ft - np.indices(input.shape, dtype=ft.dtype)
+        dt = dt.astype(np.float64)
+        if sampling is not None:
+            for ii in range(len(sampling)):
+                dt[ii, ...] *= sampling[ii]
+        np.multiply(dt, dt, dt)
+        if dt_inplace:
+            dt = np.add.reduce(dt, axis=0)
+            if distances.shape != dt.shape:
+                raise RuntimeError('distances array has wrong shape')
+            if distances.dtype.type != np.float64:
+                raise RuntimeError('distances array must be float64')
+            np.sqrt(dt, distances)
+        else:
+            dt = np.add.reduce(dt, axis=0)
+            dt = np.sqrt(dt)
+
+    # construct and return the result
+    result = []
+    if return_distances and not dt_inplace:
+        result.append(dt)
+    if return_indices and not ft_inplace:
+        result.append(ft)
+
+    if len(result) == 2:
+        return tuple(result)
+    elif len(result) == 1:
+        return result[0]
+    else:
+        return None
+
+
+def _distance_tranform_arg_check(distances_out, indices_out,
+                                 return_distances, return_indices):
+    """Raise a RuntimeError if the arguments are invalid"""
+    error_msgs = []
+    if (not return_distances) and (not return_indices):
+        error_msgs.append(
+            'at least one of return_distances/return_indices must be True')
+    if distances_out and not return_distances:
+        error_msgs.append(
+            'return_distances must be True if distances is supplied'
+        )
+    if indices_out and not return_indices:
+        error_msgs.append('return_indices must be True if indices is supplied')
+    if error_msgs:
+        raise RuntimeError(', '.join(error_msgs))
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_ni_docstrings.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_ni_docstrings.py
new file mode 100644
index 0000000000000000000000000000000000000000..e6469f2c75fcee1f74dfbbe049df8ca05b074505
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_ni_docstrings.py
@@ -0,0 +1,208 @@
+"""Docstring components common to several ndimage functions."""
+from scipy._lib import doccer
+
+__all__ = ['docfiller']
+
+
+_input_doc = (
+"""input : array_like
+    The input array.""")
+_axis_doc = (
+"""axis : int, optional
+    The axis of `input` along which to calculate. Default is -1.""")
+_output_doc = (
+"""output : array or dtype, optional
+    The array in which to place the output, or the dtype of the
+    returned array. By default an array of the same dtype as input
+    will be created.""")
+_size_foot_doc = (
+"""size : scalar or tuple, optional
+    See footprint, below. Ignored if footprint is given.
+footprint : array, optional
+    Either `size` or `footprint` must be defined. `size` gives
+    the shape that is taken from the input array, at every element
+    position, to define the input to the filter function.
+    `footprint` is a boolean array that specifies (implicitly) a
+    shape, but also which of the elements within this shape will get
+    passed to the filter function. Thus ``size=(n,m)`` is equivalent
+    to ``footprint=np.ones((n,m))``.  We adjust `size` to the number
+    of dimensions of the input array, so that, if the input array is
+    shape (10,10,10), and `size` is 2, then the actual size used is
+    (2,2,2). When `footprint` is given, `size` is ignored.""")
+_mode_reflect_doc = (
+"""mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional
+    The `mode` parameter determines how the input array is extended
+    beyond its boundaries. Default is 'reflect'. Behavior for each valid
+    value is as follows:
+
+    'reflect' (`d c b a | a b c d | d c b a`)
+        The input is extended by reflecting about the edge of the last
+        pixel. This mode is also sometimes referred to as half-sample
+        symmetric.
+
+    'constant' (`k k k k | a b c d | k k k k`)
+        The input is extended by filling all values beyond the edge with
+        the same constant value, defined by the `cval` parameter.
+
+    'nearest' (`a a a a | a b c d | d d d d`)
+        The input is extended by replicating the last pixel.
+
+    'mirror' (`d c b | a b c d | c b a`)
+        The input is extended by reflecting about the center of the last
+        pixel. This mode is also sometimes referred to as whole-sample
+        symmetric.
+
+    'wrap' (`a b c d | a b c d | a b c d`)
+        The input is extended by wrapping around to the opposite edge.
+
+    For consistency with the interpolation functions, the following mode
+    names can also be used:
+
+    'grid-mirror'
+        This is a synonym for 'reflect'.
+
+    'grid-constant'
+        This is a synonym for 'constant'.
+
+    'grid-wrap'
+        This is a synonym for 'wrap'.""")
+
+_mode_interp_constant_doc = (
+"""mode : {'reflect', 'grid-mirror', 'constant', 'grid-constant', 'nearest', \
+'mirror', 'grid-wrap', 'wrap'}, optional
+    The `mode` parameter determines how the input array is extended
+    beyond its boundaries. Default is 'constant'. Behavior for each valid
+    value is as follows (see additional plots and details on
+    :ref:`boundary modes `):
+
+    'reflect' (`d c b a | a b c d | d c b a`)
+        The input is extended by reflecting about the edge of the last
+        pixel. This mode is also sometimes referred to as half-sample
+        symmetric.
+
+    'grid-mirror'
+        This is a synonym for 'reflect'.
+
+    'constant' (`k k k k | a b c d | k k k k`)
+        The input is extended by filling all values beyond the edge with
+        the same constant value, defined by the `cval` parameter. No
+        interpolation is performed beyond the edges of the input.
+
+    'grid-constant' (`k k k k | a b c d | k k k k`)
+        The input is extended by filling all values beyond the edge with
+        the same constant value, defined by the `cval` parameter. Interpolation
+        occurs for samples outside the input's extent  as well.
+
+    'nearest' (`a a a a | a b c d | d d d d`)
+        The input is extended by replicating the last pixel.
+
+    'mirror' (`d c b | a b c d | c b a`)
+        The input is extended by reflecting about the center of the last
+        pixel. This mode is also sometimes referred to as whole-sample
+        symmetric.
+
+    'grid-wrap' (`a b c d | a b c d | a b c d`)
+        The input is extended by wrapping around to the opposite edge.
+
+    'wrap' (`d b c d | a b c d | b c a b`)
+        The input is extended by wrapping around to the opposite edge, but in a
+        way such that the last point and initial point exactly overlap. In this
+        case it is not well defined which sample will be chosen at the point of
+        overlap.""")
+_mode_interp_mirror_doc = (
+    _mode_interp_constant_doc.replace("Default is 'constant'",
+                                      "Default is 'mirror'")
+)
+assert _mode_interp_mirror_doc != _mode_interp_constant_doc, \
+    'Default not replaced'
+
+_mode_multiple_doc = (
+"""mode : str or sequence, optional
+    The `mode` parameter determines how the input array is extended
+    when the filter overlaps a border. By passing a sequence of modes
+    with length equal to the number of dimensions of the input array,
+    different modes can be specified along each axis. Default value is
+    'reflect'. The valid values and their behavior is as follows:
+
+    'reflect' (`d c b a | a b c d | d c b a`)
+        The input is extended by reflecting about the edge of the last
+        pixel. This mode is also sometimes referred to as half-sample
+        symmetric.
+
+    'constant' (`k k k k | a b c d | k k k k`)
+        The input is extended by filling all values beyond the edge with
+        the same constant value, defined by the `cval` parameter.
+
+    'nearest' (`a a a a | a b c d | d d d d`)
+        The input is extended by replicating the last pixel.
+
+    'mirror' (`d c b | a b c d | c b a`)
+        The input is extended by reflecting about the center of the last
+        pixel. This mode is also sometimes referred to as whole-sample
+        symmetric.
+
+    'wrap' (`a b c d | a b c d | a b c d`)
+        The input is extended by wrapping around to the opposite edge.
+
+    For consistency with the interpolation functions, the following mode
+    names can also be used:
+
+    'grid-constant'
+        This is a synonym for 'constant'.
+
+    'grid-mirror'
+        This is a synonym for 'reflect'.
+
+    'grid-wrap'
+        This is a synonym for 'wrap'.""")
+_cval_doc = (
+"""cval : scalar, optional
+    Value to fill past edges of input if `mode` is 'constant'. Default
+    is 0.0.""")
+_origin_doc = (
+"""origin : int, optional
+    Controls the placement of the filter on the input array's pixels.
+    A value of 0 (the default) centers the filter over the pixel, with
+    positive values shifting the filter to the left, and negative ones
+    to the right.""")
+_origin_multiple_doc = (
+"""origin : int or sequence, optional
+    Controls the placement of the filter on the input array's pixels.
+    A value of 0 (the default) centers the filter over the pixel, with
+    positive values shifting the filter to the left, and negative ones
+    to the right. By passing a sequence of origins with length equal to
+    the number of dimensions of the input array, different shifts can
+    be specified along each axis.""")
+_extra_arguments_doc = (
+"""extra_arguments : sequence, optional
+    Sequence of extra positional arguments to pass to passed function.""")
+_extra_keywords_doc = (
+"""extra_keywords : dict, optional
+    dict of extra keyword arguments to pass to passed function.""")
+_prefilter_doc = (
+"""prefilter : bool, optional
+    Determines if the input array is prefiltered with `spline_filter`
+    before interpolation. The default is True, which will create a
+    temporary `float64` array of filtered values if `order > 1`. If
+    setting this to False, the output will be slightly blurred if
+    `order > 1`, unless the input is prefiltered, i.e. it is the result
+    of calling `spline_filter` on the original input.""")
+
+docdict = {
+    'input': _input_doc,
+    'axis': _axis_doc,
+    'output': _output_doc,
+    'size_foot': _size_foot_doc,
+    'mode_interp_constant': _mode_interp_constant_doc,
+    'mode_interp_mirror': _mode_interp_mirror_doc,
+    'mode_reflect': _mode_reflect_doc,
+    'mode_multiple': _mode_multiple_doc,
+    'cval': _cval_doc,
+    'origin': _origin_doc,
+    'origin_multiple': _origin_multiple_doc,
+    'extra_arguments': _extra_arguments_doc,
+    'extra_keywords': _extra_keywords_doc,
+    'prefilter': _prefilter_doc
+    }
+
+docfiller = doccer.filldoc(docdict)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_ni_support.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_ni_support.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae8875f2ad20244604e02a0d8649a815956d4975
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/_ni_support.py
@@ -0,0 +1,119 @@
+# Copyright (C) 2003-2005 Peter J. Verveer
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above
+#    copyright notice, this list of conditions and the following
+#    disclaimer in the documentation and/or other materials provided
+#    with the distribution.
+#
+# 3. The name of the author may not be used to endorse or promote
+#    products derived from this software without specific prior
+#    written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from collections.abc import Iterable
+import operator
+import warnings
+import numpy as np
+
+
+def _extend_mode_to_code(mode):
+    """Convert an extension mode to the corresponding integer code.
+    """
+    if mode == 'nearest':
+        return 0
+    elif mode == 'wrap':
+        return 1
+    elif mode in ['reflect', 'grid-mirror']:
+        return 2
+    elif mode == 'mirror':
+        return 3
+    elif mode == 'constant':
+        return 4
+    elif mode == 'grid-wrap':
+        return 5
+    elif mode == 'grid-constant':
+        return 6
+    else:
+        raise RuntimeError('boundary mode not supported')
+
+
+def _normalize_sequence(input, rank):
+    """If input is a scalar, create a sequence of length equal to the
+    rank by duplicating the input. If input is a sequence,
+    check if its length is equal to the length of array.
+    """
+    is_str = isinstance(input, str)
+    if not is_str and isinstance(input, Iterable):
+        normalized = list(input)
+        if len(normalized) != rank:
+            err = "sequence argument must have length equal to input rank"
+            raise RuntimeError(err)
+    else:
+        normalized = [input] * rank
+    return normalized
+
+
+def _get_output(output, input, shape=None, complex_output=False):
+    if shape is None:
+        shape = input.shape
+    if output is None:
+        if not complex_output:
+            output = np.zeros(shape, dtype=input.dtype.name)
+        else:
+            complex_type = np.promote_types(input.dtype, np.complex64)
+            output = np.zeros(shape, dtype=complex_type)
+    elif isinstance(output, (type, np.dtype)):
+        # Classes (like `np.float32`) and dtypes are interpreted as dtype
+        if complex_output and np.dtype(output).kind != 'c':
+            warnings.warn("promoting specified output dtype to complex", stacklevel=3)
+            output = np.promote_types(output, np.complex64)
+        output = np.zeros(shape, dtype=output)
+    elif isinstance(output, str):
+        output = np.dtype(output)
+        if complex_output and output.kind != 'c':
+            raise RuntimeError("output must have complex dtype")
+        elif not issubclass(output.type, np.number):
+            raise RuntimeError("output must have numeric dtype")
+        output = np.zeros(shape, dtype=output)
+    elif output.shape != shape:
+        raise RuntimeError("output shape not correct")
+    elif complex_output and output.dtype.kind != 'c':
+        raise RuntimeError("output must have complex dtype")
+    return output
+
+
+def _check_axes(axes, ndim):
+    if axes is None:
+        return tuple(range(ndim))
+    elif np.isscalar(axes):
+        axes = (operator.index(axes),)
+    elif isinstance(axes, Iterable):
+        for ax in axes:
+            axes = tuple(operator.index(ax) for ax in axes)
+            if ax < -ndim or ax > ndim - 1:
+                raise ValueError(f"specified axis: {ax} is out of range")
+        axes = tuple(ax % ndim if ax < 0 else ax for ax in axes)
+    else:
+        message = "axes must be an integer, iterable of integers, or None"
+        raise ValueError(message)
+    if len(tuple(set(axes))) != len(axes):
+        raise ValueError("axes must be unique")
+    return axes
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/filters.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..e16d9d279a9585b2454c46ee09cf22143de833a6
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/filters.py
@@ -0,0 +1,27 @@
+# This file is not meant for public use and will be removed in SciPy v2.0.0.
+# Use the `scipy.ndimage` namespace for importing the functions
+# included below.
+
+from scipy._lib.deprecation import _sub_module_deprecation
+
+
+__all__ = [  # noqa: F822
+    'correlate1d', 'convolve1d', 'gaussian_filter1d',
+    'gaussian_filter', 'prewitt', 'sobel', 'generic_laplace',
+    'laplace', 'gaussian_laplace', 'generic_gradient_magnitude',
+    'gaussian_gradient_magnitude', 'correlate', 'convolve',
+    'uniform_filter1d', 'uniform_filter', 'minimum_filter1d',
+    'maximum_filter1d', 'minimum_filter', 'maximum_filter',
+    'rank_filter', 'median_filter', 'percentile_filter',
+    'generic_filter1d', 'generic_filter'
+]
+
+
+def __dir__():
+    return __all__
+
+
+def __getattr__(name):
+    return _sub_module_deprecation(sub_package='ndimage', module='filters',
+                                   private_modules=['_filters'], all=__all__,
+                                   attribute=name)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/fourier.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/fourier.py
new file mode 100644
index 0000000000000000000000000000000000000000..73c49bd52d9a446ce0fe25d9e15b8de68fbd46fb
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/fourier.py
@@ -0,0 +1,21 @@
+# This file is not meant for public use and will be removed in SciPy v2.0.0.
+# Use the `scipy.ndimage` namespace for importing the functions
+# included below.
+
+from scipy._lib.deprecation import _sub_module_deprecation
+
+
+__all__ = [  # noqa: F822
+    'fourier_gaussian', 'fourier_uniform',
+    'fourier_ellipsoid', 'fourier_shift'
+]
+
+
+def __dir__():
+    return __all__
+
+
+def __getattr__(name):
+    return _sub_module_deprecation(sub_package='ndimage', module='fourier',
+                                   private_modules=['_fourier'], all=__all__,
+                                   attribute=name)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/interpolation.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/interpolation.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2739c60c51037487ae8892c407e2f3d7870d5da
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/interpolation.py
@@ -0,0 +1,22 @@
+# This file is not meant for public use and will be removed in SciPy v2.0.0.
+# Use the `scipy.ndimage` namespace for importing the functions
+# included below.
+
+from scipy._lib.deprecation import _sub_module_deprecation
+
+
+__all__ = [  # noqa: F822
+    'spline_filter1d', 'spline_filter',
+    'geometric_transform', 'map_coordinates',
+    'affine_transform', 'shift', 'zoom', 'rotate',
+]
+
+
+def __dir__():
+    return __all__
+
+
+def __getattr__(name):
+    return _sub_module_deprecation(sub_package='ndimage', module='interpolation',
+                                   private_modules=['_interpolation'], all=__all__,
+                                   attribute=name)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/measurements.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/measurements.py
new file mode 100644
index 0000000000000000000000000000000000000000..22f76b01840ffb829205bd1d28a7ad1f9ac5db61
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/measurements.py
@@ -0,0 +1,24 @@
+# This file is not meant for public use and will be removed in SciPy v2.0.0.
+# Use the `scipy.ndimage` namespace for importing the functions
+# included below.
+
+from scipy._lib.deprecation import _sub_module_deprecation
+
+
+__all__ = [  # noqa: F822
+    'label', 'find_objects', 'labeled_comprehension',
+    'sum', 'mean', 'variance', 'standard_deviation',
+    'minimum', 'maximum', 'median', 'minimum_position',
+    'maximum_position', 'extrema', 'center_of_mass',
+    'histogram', 'watershed_ift', 'sum_labels'
+]
+
+
+def __dir__():
+    return __all__
+
+
+def __getattr__(name):
+    return _sub_module_deprecation(sub_package='ndimage', module='measurements',
+                                   private_modules=['_measurements'], all=__all__,
+                                   attribute=name)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/morphology.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/morphology.py
new file mode 100644
index 0000000000000000000000000000000000000000..e522e7df3a4b06b7e04ed8c2d0ecaff2a98b951d
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/morphology.py
@@ -0,0 +1,27 @@
+# This file is not meant for public use and will be removed in SciPy v2.0.0.
+# Use the `scipy.ndimage` namespace for importing the functions
+# included below.
+
+from scipy._lib.deprecation import _sub_module_deprecation
+
+
+__all__ = [  # noqa: F822
+    'iterate_structure', 'generate_binary_structure',
+    'binary_erosion', 'binary_dilation', 'binary_opening',
+    'binary_closing', 'binary_hit_or_miss', 'binary_propagation',
+    'binary_fill_holes', 'grey_erosion', 'grey_dilation',
+    'grey_opening', 'grey_closing', 'morphological_gradient',
+    'morphological_laplace', 'white_tophat', 'black_tophat',
+    'distance_transform_bf', 'distance_transform_cdt',
+    'distance_transform_edt'
+]
+
+
+def __dir__():
+    return __all__
+
+
+def __getattr__(name):
+    return _sub_module_deprecation(sub_package='ndimage', module='morphology',
+                                   private_modules=['_morphology'], all=__all__,
+                                   attribute=name)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__init__.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1e40498b438bc7555960d68993447f901c3efbc8
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__init__.py
@@ -0,0 +1,13 @@
+from __future__ import annotations
+import numpy as np
+
+# list of numarray data types
+integer_types: list[type] = [
+    np.int8, np.uint8, np.int16, np.uint16,
+    np.int32, np.uint32, np.int64, np.uint64]
+
+float_types: list[type] = [np.float32, np.float64]
+
+complex_types: list[type] = [np.complex64, np.complex128]
+
+types: list[type] = integer_types + float_types
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/__init__.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..fcdddbcaa5d8350040a488cba2390b984db9e3b7
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/__init__.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_c_api.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_c_api.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9f4f1c9e20ea1b51057fa093425dbfbab7998e03
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_c_api.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_datatypes.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_datatypes.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b2ec2510f586d1561a633ed5519ac16fae7a37c0
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_datatypes.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_filters.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_filters.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d30f8ad26a9c12a99451313fb85ef93be3fac03d
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_filters.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_fourier.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_fourier.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0b6ba34778271ce6adf201d2a1c43a2795a8daa6
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_fourier.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_interpolation.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_interpolation.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a7c94cdbfe656dc16f84dd7bcaeaa9b4ce4a5c8a
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_interpolation.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_measurements.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_measurements.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c4605e3a24678ec7f6d5a8d2bb50e8c0f541b1b5
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_measurements.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_morphology.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_morphology.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..44a296f06a0fc2e7499233cc02ca098997772835
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_morphology.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_ni_support.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_ni_support.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b0461024887ef55947ba962d80ca5233ff161dd3
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_ni_support.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_splines.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_splines.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ad512109fdd8bbba8dc9caa4f11a7833d9cc4ea9
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/__pycache__/test_splines.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/data/label_inputs.txt b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/data/label_inputs.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6c3cff3b12cec4ad050b31cc5d5c327f32784447
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/data/label_inputs.txt
@@ -0,0 +1,21 @@
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 0 1 1 1
+1 1 0 0 0 1 1
+1 0 1 0 1 0 1
+0 0 0 1 0 0 0
+1 0 1 0 1 0 1
+1 1 0 0 0 1 1
+1 1 1 0 1 1 1
+1 0 1 1 1 0 1
+0 0 0 1 0 0 0
+1 0 0 1 0 0 1
+1 1 1 1 1 1 1
+1 0 0 1 0 0 1
+0 0 0 1 0 0 0
+1 0 1 1 1 0 1
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/data/label_results.txt b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/data/label_results.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c239b0369c9df3e06df9a2fbf048faec2f84941f
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/data/label_results.txt
@@ -0,0 +1,294 @@
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+2 2 2 2 2 2 2
+3 3 3 3 3 3 3
+4 4 4 4 4 4 4
+5 5 5 5 5 5 5
+6 6 6 6 6 6 6
+7 7 7 7 7 7 7
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 2 3 4 5 6 7
+8 9 10 11 12 13 14
+15 16 17 18 19 20 21
+22 23 24 25 26 27 28
+29 30 31 32 33 34 35
+36 37 38 39 40 41 42
+43 44 45 46 47 48 49
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 2 3 4 5 6 7
+8 1 2 3 4 5 6
+9 8 1 2 3 4 5
+10 9 8 1 2 3 4
+11 10 9 8 1 2 3
+12 11 10 9 8 1 2
+13 12 11 10 9 8 1
+1 2 3 4 5 6 7
+1 2 3 4 5 6 7
+1 2 3 4 5 6 7
+1 2 3 4 5 6 7
+1 2 3 4 5 6 7
+1 2 3 4 5 6 7
+1 2 3 4 5 6 7
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 2 1 2 1 2 1
+2 1 2 1 2 1 2
+1 2 1 2 1 2 1
+2 1 2 1 2 1 2
+1 2 1 2 1 2 1
+2 1 2 1 2 1 2
+1 2 1 2 1 2 1
+1 2 3 4 5 6 7
+2 3 4 5 6 7 8
+3 4 5 6 7 8 9
+4 5 6 7 8 9 10
+5 6 7 8 9 10 11
+6 7 8 9 10 11 12
+7 8 9 10 11 12 13
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 1 1 1 1
+1 1 1 0 2 2 2
+1 1 0 0 0 2 2
+1 0 3 0 2 0 4
+0 0 0 2 0 0 0
+5 0 2 0 6 0 7
+2 2 0 0 0 7 7
+2 2 2 0 7 7 7
+1 1 1 0 2 2 2
+1 1 0 0 0 2 2
+3 0 1 0 4 0 2
+0 0 0 1 0 0 0
+5 0 6 0 1 0 7
+5 5 0 0 0 1 1
+5 5 5 0 1 1 1
+1 1 1 0 2 2 2
+3 3 0 0 0 4 4
+5 0 6 0 7 0 8
+0 0 0 9 0 0 0
+10 0 11 0 12 0 13
+14 14 0 0 0 15 15
+16 16 16 0 17 17 17
+1 1 1 0 2 3 3
+1 1 0 0 0 3 3
+1 0 4 0 3 0 3
+0 0 0 3 0 0 0
+3 0 3 0 5 0 6
+3 3 0 0 0 6 6
+3 3 7 0 6 6 6
+1 2 3 0 4 5 6
+7 8 0 0 0 9 10
+11 0 12 0 13 0 14
+0 0 0 15 0 0 0
+16 0 17 0 18 0 19
+20 21 0 0 0 22 23
+24 25 26 0 27 28 29
+1 1 1 0 2 2 2
+1 1 0 0 0 2 2
+1 0 3 0 2 0 2
+0 0 0 2 0 0 0
+2 0 2 0 4 0 5
+2 2 0 0 0 5 5
+2 2 2 0 5 5 5
+1 1 1 0 2 2 2
+1 1 0 0 0 2 2
+1 0 3 0 4 0 2
+0 0 0 5 0 0 0
+6 0 7 0 8 0 9
+6 6 0 0 0 9 9
+6 6 6 0 9 9 9
+1 2 3 0 4 5 6
+7 1 0 0 0 4 5
+8 0 1 0 9 0 4
+0 0 0 1 0 0 0
+10 0 11 0 1 0 12
+13 10 0 0 0 1 14
+15 13 10 0 16 17 1
+1 2 3 0 4 5 6
+1 2 0 0 0 5 6
+1 0 7 0 8 0 6
+0 0 0 9 0 0 0
+10 0 11 0 12 0 13
+10 14 0 0 0 15 13
+10 14 16 0 17 15 13
+1 1 1 0 1 1 1
+1 1 0 0 0 1 1
+1 0 1 0 1 0 1
+0 0 0 1 0 0 0
+1 0 1 0 1 0 1
+1 1 0 0 0 1 1
+1 1 1 0 1 1 1
+1 1 2 0 3 3 3
+1 1 0 0 0 3 3
+1 0 1 0 4 0 3
+0 0 0 1 0 0 0
+5 0 6 0 1 0 1
+5 5 0 0 0 1 1
+5 5 5 0 7 1 1
+1 2 1 0 1 3 1
+2 1 0 0 0 1 3
+1 0 1 0 1 0 1
+0 0 0 1 0 0 0
+1 0 1 0 1 0 1
+4 1 0 0 0 1 5
+1 4 1 0 1 5 1
+1 2 3 0 4 5 6
+2 3 0 0 0 6 7
+3 0 8 0 6 0 9
+0 0 0 6 0 0 0
+10 0 6 0 11 0 12
+13 6 0 0 0 12 14
+6 15 16 0 12 14 17
+1 1 1 0 2 2 2
+1 1 0 0 0 2 2
+1 0 1 0 3 0 2
+0 0 0 1 0 0 0
+4 0 5 0 1 0 1
+4 4 0 0 0 1 1
+4 4 4 0 1 1 1
+1 0 2 2 2 0 3
+0 0 0 2 0 0 0
+4 0 0 5 0 0 5
+5 5 5 5 5 5 5
+5 0 0 5 0 0 6
+0 0 0 7 0 0 0
+8 0 7 7 7 0 9
+1 0 2 2 2 0 3
+0 0 0 2 0 0 0
+4 0 0 4 0 0 5
+4 4 4 4 4 4 4
+6 0 0 4 0 0 4
+0 0 0 7 0 0 0
+8 0 7 7 7 0 9
+1 0 2 2 2 0 3
+0 0 0 4 0 0 0
+5 0 0 6 0 0 7
+8 8 8 8 8 8 8
+9 0 0 10 0 0 11
+0 0 0 12 0 0 0
+13 0 14 14 14 0 15
+1 0 2 3 3 0 4
+0 0 0 3 0 0 0
+5 0 0 3 0 0 6
+5 5 3 3 3 6 6
+5 0 0 3 0 0 6
+0 0 0 3 0 0 0
+7 0 3 3 8 0 9
+1 0 2 3 4 0 5
+0 0 0 6 0 0 0
+7 0 0 8 0 0 9
+10 11 12 13 14 15 16
+17 0 0 18 0 0 19
+0 0 0 20 0 0 0
+21 0 22 23 24 0 25
+1 0 2 2 2 0 3
+0 0 0 2 0 0 0
+2 0 0 2 0 0 2
+2 2 2 2 2 2 2
+2 0 0 2 0 0 2
+0 0 0 2 0 0 0
+4 0 2 2 2 0 5
+1 0 2 2 2 0 3
+0 0 0 2 0 0 0
+2 0 0 2 0 0 2
+2 2 2 2 2 2 2
+2 0 0 2 0 0 2
+0 0 0 2 0 0 0
+4 0 2 2 2 0 5
+1 0 2 3 4 0 5
+0 0 0 2 0 0 0
+6 0 0 7 0 0 8
+9 6 10 11 7 12 13
+14 0 0 10 0 0 12
+0 0 0 15 0 0 0
+16 0 17 18 15 0 19
+1 0 2 3 4 0 5
+0 0 0 3 0 0 0
+6 0 0 3 0 0 7
+6 8 9 3 10 11 7
+6 0 0 3 0 0 7
+0 0 0 3 0 0 0
+12 0 13 3 14 0 15
+1 0 2 2 2 0 3
+0 0 0 2 0 0 0
+2 0 0 2 0 0 2
+2 2 2 2 2 2 2
+2 0 0 2 0 0 2
+0 0 0 2 0 0 0
+4 0 2 2 2 0 5
+1 0 2 2 3 0 4
+0 0 0 2 0 0 0
+5 0 0 2 0 0 6
+5 5 2 2 2 6 6
+5 0 0 2 0 0 6
+0 0 0 2 0 0 0
+7 0 8 2 2 0 9
+1 0 2 3 2 0 4
+0 0 0 2 0 0 0
+5 0 0 6 0 0 7
+8 5 6 9 6 7 10
+5 0 0 6 0 0 7
+0 0 0 11 0 0 0
+12 0 11 13 11 0 14
+1 0 2 3 4 0 5
+0 0 0 4 0 0 0
+6 0 0 7 0 0 8
+9 10 7 11 12 8 13
+10 0 0 12 0 0 14
+0 0 0 15 0 0 0
+16 0 15 17 18 0 19
+1 0 2 2 2 0 3
+0 0 0 2 0 0 0
+2 0 0 2 0 0 2
+2 2 2 2 2 2 2
+2 0 0 2 0 0 2
+0 0 0 2 0 0 0
+4 0 2 2 2 0 5
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/data/label_strels.txt b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/data/label_strels.txt
new file mode 100644
index 0000000000000000000000000000000000000000..35ae8121364d4fb3292c11f2a72333f456fa9c0a
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/data/label_strels.txt
@@ -0,0 +1,42 @@
+0 0 1
+1 1 1
+1 0 0
+1 0 0
+1 1 1
+0 0 1
+0 0 0
+1 1 1
+0 0 0
+0 1 1
+0 1 0
+1 1 0
+0 0 0
+0 0 0
+0 0 0
+0 1 1
+1 1 1
+1 1 0
+0 1 0
+1 1 1
+0 1 0
+1 0 0
+0 1 0
+0 0 1
+0 1 0
+0 1 0
+0 1 0
+1 1 1
+1 1 1
+1 1 1
+1 1 0
+0 1 0
+0 1 1
+1 0 1
+0 1 0
+1 0 1
+0 0 1
+0 1 0
+1 0 0
+1 1 0
+1 1 1
+0 1 1
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/dots.png b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/dots.png
new file mode 100644
index 0000000000000000000000000000000000000000..640030ca1362bf0364ec2b180694ac6198b835c7
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/dots.png differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_c_api.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_c_api.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed52ed8477056176e1f5aacbf681b12b0153fee6
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_c_api.py
@@ -0,0 +1,102 @@
+import numpy as np
+from numpy.testing import assert_allclose
+
+from scipy import ndimage
+from scipy.ndimage import _ctest
+from scipy.ndimage import _cytest
+from scipy._lib._ccallback import LowLevelCallable
+
+FILTER1D_FUNCTIONS = [
+    lambda filter_size: _ctest.filter1d(filter_size),
+    lambda filter_size: _cytest.filter1d(filter_size, with_signature=False),
+    lambda filter_size: LowLevelCallable(
+                            _cytest.filter1d(filter_size, with_signature=True)
+                        ),
+    lambda filter_size: LowLevelCallable.from_cython(
+                            _cytest, "_filter1d",
+                            _cytest.filter1d_capsule(filter_size),
+                        ),
+]
+
+FILTER2D_FUNCTIONS = [
+    lambda weights: _ctest.filter2d(weights),
+    lambda weights: _cytest.filter2d(weights, with_signature=False),
+    lambda weights: LowLevelCallable(_cytest.filter2d(weights, with_signature=True)),
+    lambda weights: LowLevelCallable.from_cython(_cytest,
+                                                 "_filter2d",
+                                                 _cytest.filter2d_capsule(weights),),
+]
+
+TRANSFORM_FUNCTIONS = [
+    lambda shift: _ctest.transform(shift),
+    lambda shift: _cytest.transform(shift, with_signature=False),
+    lambda shift: LowLevelCallable(_cytest.transform(shift, with_signature=True)),
+    lambda shift: LowLevelCallable.from_cython(_cytest,
+                                               "_transform",
+                                               _cytest.transform_capsule(shift),),
+]
+
+
+def test_generic_filter():
+    def filter2d(footprint_elements, weights):
+        return (weights*footprint_elements).sum()
+
+    def check(j):
+        func = FILTER2D_FUNCTIONS[j]
+
+        im = np.ones((20, 20))
+        im[:10,:10] = 0
+        footprint = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]])
+        footprint_size = np.count_nonzero(footprint)
+        weights = np.ones(footprint_size)/footprint_size
+
+        res = ndimage.generic_filter(im, func(weights),
+                                     footprint=footprint)
+        std = ndimage.generic_filter(im, filter2d, footprint=footprint,
+                                     extra_arguments=(weights,))
+        assert_allclose(res, std, err_msg=f"#{j} failed")
+
+    for j, func in enumerate(FILTER2D_FUNCTIONS):
+        check(j)
+
+
+def test_generic_filter1d():
+    def filter1d(input_line, output_line, filter_size):
+        for i in range(output_line.size):
+            output_line[i] = 0
+            for j in range(filter_size):
+                output_line[i] += input_line[i+j]
+        output_line /= filter_size
+
+    def check(j):
+        func = FILTER1D_FUNCTIONS[j]
+
+        im = np.tile(np.hstack((np.zeros(10), np.ones(10))), (10, 1))
+        filter_size = 3
+
+        res = ndimage.generic_filter1d(im, func(filter_size),
+                                       filter_size)
+        std = ndimage.generic_filter1d(im, filter1d, filter_size,
+                                       extra_arguments=(filter_size,))
+        assert_allclose(res, std, err_msg=f"#{j} failed")
+
+    for j, func in enumerate(FILTER1D_FUNCTIONS):
+        check(j)
+
+
+def test_geometric_transform():
+    def transform(output_coordinates, shift):
+        return output_coordinates[0] - shift, output_coordinates[1] - shift
+
+    def check(j):
+        func = TRANSFORM_FUNCTIONS[j]
+
+        im = np.arange(12).reshape(4, 3).astype(np.float64)
+        shift = 0.5
+
+        res = ndimage.geometric_transform(im, func(shift))
+        std = ndimage.geometric_transform(im, transform, extra_arguments=(shift,))
+        assert_allclose(res, std, err_msg=f"#{j} failed")
+
+    for j, func in enumerate(TRANSFORM_FUNCTIONS):
+        check(j)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_datatypes.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_datatypes.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd9382a16ada38a6d3059d54ad765c2e0f74b7c1
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_datatypes.py
@@ -0,0 +1,66 @@
+""" Testing data types for ndimage calls
+"""
+import numpy as np
+from numpy.testing import assert_array_almost_equal, assert_
+import pytest
+
+from scipy import ndimage
+
+
+def test_map_coordinates_dts():
+    # check that ndimage accepts different data types for interpolation
+    data = np.array([[4, 1, 3, 2],
+                     [7, 6, 8, 5],
+                     [3, 5, 3, 6]])
+    shifted_data = np.array([[0, 0, 0, 0],
+                             [0, 4, 1, 3],
+                             [0, 7, 6, 8]])
+    idx = np.indices(data.shape)
+    dts = (np.uint8, np.uint16, np.uint32, np.uint64,
+           np.int8, np.int16, np.int32, np.int64,
+           np.intp, np.uintp, np.float32, np.float64)
+    for order in range(0, 6):
+        for data_dt in dts:
+            these_data = data.astype(data_dt)
+            for coord_dt in dts:
+                # affine mapping
+                mat = np.eye(2, dtype=coord_dt)
+                off = np.zeros((2,), dtype=coord_dt)
+                out = ndimage.affine_transform(these_data, mat, off)
+                assert_array_almost_equal(these_data, out)
+                # map coordinates
+                coords_m1 = idx.astype(coord_dt) - 1
+                coords_p10 = idx.astype(coord_dt) + 10
+                out = ndimage.map_coordinates(these_data, coords_m1, order=order)
+                assert_array_almost_equal(out, shifted_data)
+                # check constant fill works
+                out = ndimage.map_coordinates(these_data, coords_p10, order=order)
+                assert_array_almost_equal(out, np.zeros((3,4)))
+            # check shift and zoom
+            out = ndimage.shift(these_data, 1)
+            assert_array_almost_equal(out, shifted_data)
+            out = ndimage.zoom(these_data, 1)
+            assert_array_almost_equal(these_data, out)
+
+
+@pytest.mark.xfail(True, reason="Broken on many platforms")
+def test_uint64_max():
+    # Test interpolation respects uint64 max.  Reported to fail at least on
+    # win32 (due to the 32 bit visual C compiler using signed int64 when
+    # converting between uint64 to double) and Debian on s390x.
+    # Interpolation is always done in double precision floating point, so
+    # we use the largest uint64 value for which int(float(big)) still fits
+    # in a uint64.
+    # This test was last enabled on macOS only, and there it started failing
+    # on arm64 as well (see gh-19117).
+    big = 2**64 - 1025
+    arr = np.array([big, big, big], dtype=np.uint64)
+    # Tests geometric transform (map_coordinates, affine_transform)
+    inds = np.indices(arr.shape) - 0.1
+    x = ndimage.map_coordinates(arr, inds)
+    assert_(x[1] == int(float(big)))
+    assert_(x[2] == int(float(big)))
+    # Tests zoom / shift
+    x = ndimage.shift(arr, 0.1)
+    assert_(x[1] == int(float(big)))
+    assert_(x[2] == int(float(big)))
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_filters.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..6782cf463cfffa857be731bceb5d87377084864c
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_filters.py
@@ -0,0 +1,2214 @@
+''' Some tests for filters '''
+import functools
+import itertools
+import math
+import numpy as np
+
+from numpy.testing import (assert_equal, assert_allclose,
+                           assert_array_almost_equal,
+                           assert_array_equal, assert_almost_equal,
+                           suppress_warnings, assert_)
+import pytest
+from pytest import raises as assert_raises
+
+from scipy import ndimage
+from scipy.ndimage._filters import _gaussian_kernel1d
+
+from . import types, float_types, complex_types
+
+
+def sumsq(a, b):
+    return math.sqrt(((a - b)**2).sum())
+
+
+def _complex_correlate(array, kernel, real_dtype, convolve=False,
+                       mode="reflect", cval=0, ):
+    """Utility to perform a reference complex-valued convolutions.
+
+    When convolve==False, correlation is performed instead
+    """
+    array = np.asarray(array)
+    kernel = np.asarray(kernel)
+    complex_array = array.dtype.kind == 'c'
+    complex_kernel = kernel.dtype.kind == 'c'
+    if array.ndim == 1:
+        func = ndimage.convolve1d if convolve else ndimage.correlate1d
+    else:
+        func = ndimage.convolve if convolve else ndimage.correlate
+    if not convolve:
+        kernel = kernel.conj()
+    if complex_array and complex_kernel:
+        # use: real(cval) for array.real component
+        #      imag(cval) for array.imag component
+        output = (
+            func(array.real, kernel.real, output=real_dtype,
+                 mode=mode, cval=np.real(cval)) -
+            func(array.imag, kernel.imag, output=real_dtype,
+                 mode=mode, cval=np.imag(cval)) +
+            1j * func(array.imag, kernel.real, output=real_dtype,
+                      mode=mode, cval=np.imag(cval)) +
+            1j * func(array.real, kernel.imag, output=real_dtype,
+                      mode=mode, cval=np.real(cval))
+        )
+    elif complex_array:
+        output = (
+            func(array.real, kernel, output=real_dtype, mode=mode,
+                 cval=np.real(cval)) +
+            1j * func(array.imag, kernel, output=real_dtype, mode=mode,
+                      cval=np.imag(cval))
+        )
+    elif complex_kernel:
+        # real array so cval is real too
+        output = (
+            func(array, kernel.real, output=real_dtype, mode=mode, cval=cval) +
+            1j * func(array, kernel.imag, output=real_dtype, mode=mode,
+                      cval=cval)
+        )
+    return output
+
+
+def _cases_axes_tuple_length_mismatch():
+    # Generate combinations of filter function, valid kwargs, and
+    # keyword-value pairs for which the value will become with mismatched
+    # (invalid) size
+    filter_func = ndimage.gaussian_filter
+    kwargs = dict(radius=3, mode='constant', sigma=1.0, order=0)
+    for key, val in kwargs.items():
+        yield filter_func, kwargs, key, val
+
+    filter_funcs = [ndimage.uniform_filter, ndimage.minimum_filter,
+                    ndimage.maximum_filter]
+    kwargs = dict(size=3, mode='constant', origin=0)
+    for filter_func in filter_funcs:
+        for key, val in kwargs.items():
+            yield filter_func, kwargs, key, val
+
+
+class TestNdimageFilters:
+
+    def _validate_complex(self, array, kernel, type2, mode='reflect', cval=0):
+        # utility for validating complex-valued correlations
+        real_dtype = np.asarray([], dtype=type2).real.dtype
+        expected = _complex_correlate(
+            array, kernel, real_dtype, convolve=False, mode=mode, cval=cval
+        )
+
+        if array.ndim == 1:
+            correlate = functools.partial(ndimage.correlate1d, axis=-1,
+                                          mode=mode, cval=cval)
+            convolve = functools.partial(ndimage.convolve1d, axis=-1,
+                                         mode=mode, cval=cval)
+        else:
+            correlate = functools.partial(ndimage.correlate, mode=mode,
+                                          cval=cval)
+            convolve = functools.partial(ndimage.convolve, mode=mode,
+                                          cval=cval)
+
+        # test correlate output dtype
+        output = correlate(array, kernel, output=type2)
+        assert_array_almost_equal(expected, output)
+        assert_equal(output.dtype.type, type2)
+
+        # test correlate with pre-allocated output
+        output = np.zeros_like(array, dtype=type2)
+        correlate(array, kernel, output=output)
+        assert_array_almost_equal(expected, output)
+
+        # test convolve output dtype
+        output = convolve(array, kernel, output=type2)
+        expected = _complex_correlate(
+            array, kernel, real_dtype, convolve=True, mode=mode, cval=cval,
+        )
+        assert_array_almost_equal(expected, output)
+        assert_equal(output.dtype.type, type2)
+
+        # convolve with pre-allocated output
+        convolve(array, kernel, output=output)
+        assert_array_almost_equal(expected, output)
+        assert_equal(output.dtype.type, type2)
+
+        # warns if the output is not a complex dtype
+        with pytest.warns(UserWarning,
+                          match="promoting specified output dtype to complex"):
+            correlate(array, kernel, output=real_dtype)
+
+        with pytest.warns(UserWarning,
+                          match="promoting specified output dtype to complex"):
+            convolve(array, kernel, output=real_dtype)
+
+        # raises if output array is provided, but is not complex-valued
+        output_real = np.zeros_like(array, dtype=real_dtype)
+        with assert_raises(RuntimeError):
+            correlate(array, kernel, output=output_real)
+
+        with assert_raises(RuntimeError):
+            convolve(array, kernel, output=output_real)
+
+    def test_correlate01(self):
+        array = np.array([1, 2])
+        weights = np.array([2])
+        expected = [2, 4]
+
+        output = ndimage.correlate(array, weights)
+        assert_array_almost_equal(output, expected)
+
+        output = ndimage.convolve(array, weights)
+        assert_array_almost_equal(output, expected)
+
+        output = ndimage.correlate1d(array, weights)
+        assert_array_almost_equal(output, expected)
+
+        output = ndimage.convolve1d(array, weights)
+        assert_array_almost_equal(output, expected)
+
+    def test_correlate01_overlap(self):
+        array = np.arange(256).reshape(16, 16)
+        weights = np.array([2])
+        expected = 2 * array
+
+        ndimage.correlate1d(array, weights, output=array)
+        assert_array_almost_equal(array, expected)
+
+    def test_correlate02(self):
+        array = np.array([1, 2, 3])
+        kernel = np.array([1])
+
+        output = ndimage.correlate(array, kernel)
+        assert_array_almost_equal(array, output)
+
+        output = ndimage.convolve(array, kernel)
+        assert_array_almost_equal(array, output)
+
+        output = ndimage.correlate1d(array, kernel)
+        assert_array_almost_equal(array, output)
+
+        output = ndimage.convolve1d(array, kernel)
+        assert_array_almost_equal(array, output)
+
+    def test_correlate03(self):
+        array = np.array([1])
+        weights = np.array([1, 1])
+        expected = [2]
+
+        output = ndimage.correlate(array, weights)
+        assert_array_almost_equal(output, expected)
+
+        output = ndimage.convolve(array, weights)
+        assert_array_almost_equal(output, expected)
+
+        output = ndimage.correlate1d(array, weights)
+        assert_array_almost_equal(output, expected)
+
+        output = ndimage.convolve1d(array, weights)
+        assert_array_almost_equal(output, expected)
+
+    def test_correlate04(self):
+        array = np.array([1, 2])
+        tcor = [2, 3]
+        tcov = [3, 4]
+        weights = np.array([1, 1])
+        output = ndimage.correlate(array, weights)
+        assert_array_almost_equal(output, tcor)
+        output = ndimage.convolve(array, weights)
+        assert_array_almost_equal(output, tcov)
+        output = ndimage.correlate1d(array, weights)
+        assert_array_almost_equal(output, tcor)
+        output = ndimage.convolve1d(array, weights)
+        assert_array_almost_equal(output, tcov)
+
+    def test_correlate05(self):
+        array = np.array([1, 2, 3])
+        tcor = [2, 3, 5]
+        tcov = [3, 5, 6]
+        kernel = np.array([1, 1])
+        output = ndimage.correlate(array, kernel)
+        assert_array_almost_equal(tcor, output)
+        output = ndimage.convolve(array, kernel)
+        assert_array_almost_equal(tcov, output)
+        output = ndimage.correlate1d(array, kernel)
+        assert_array_almost_equal(tcor, output)
+        output = ndimage.convolve1d(array, kernel)
+        assert_array_almost_equal(tcov, output)
+
+    def test_correlate06(self):
+        array = np.array([1, 2, 3])
+        tcor = [9, 14, 17]
+        tcov = [7, 10, 15]
+        weights = np.array([1, 2, 3])
+        output = ndimage.correlate(array, weights)
+        assert_array_almost_equal(output, tcor)
+        output = ndimage.convolve(array, weights)
+        assert_array_almost_equal(output, tcov)
+        output = ndimage.correlate1d(array, weights)
+        assert_array_almost_equal(output, tcor)
+        output = ndimage.convolve1d(array, weights)
+        assert_array_almost_equal(output, tcov)
+
+    def test_correlate07(self):
+        array = np.array([1, 2, 3])
+        expected = [5, 8, 11]
+        weights = np.array([1, 2, 1])
+        output = ndimage.correlate(array, weights)
+        assert_array_almost_equal(output, expected)
+        output = ndimage.convolve(array, weights)
+        assert_array_almost_equal(output, expected)
+        output = ndimage.correlate1d(array, weights)
+        assert_array_almost_equal(output, expected)
+        output = ndimage.convolve1d(array, weights)
+        assert_array_almost_equal(output, expected)
+
+    def test_correlate08(self):
+        array = np.array([1, 2, 3])
+        tcor = [1, 2, 5]
+        tcov = [3, 6, 7]
+        weights = np.array([1, 2, -1])
+        output = ndimage.correlate(array, weights)
+        assert_array_almost_equal(output, tcor)
+        output = ndimage.convolve(array, weights)
+        assert_array_almost_equal(output, tcov)
+        output = ndimage.correlate1d(array, weights)
+        assert_array_almost_equal(output, tcor)
+        output = ndimage.convolve1d(array, weights)
+        assert_array_almost_equal(output, tcov)
+
+    def test_correlate09(self):
+        array = []
+        kernel = np.array([1, 1])
+        output = ndimage.correlate(array, kernel)
+        assert_array_almost_equal(array, output)
+        output = ndimage.convolve(array, kernel)
+        assert_array_almost_equal(array, output)
+        output = ndimage.correlate1d(array, kernel)
+        assert_array_almost_equal(array, output)
+        output = ndimage.convolve1d(array, kernel)
+        assert_array_almost_equal(array, output)
+
+    def test_correlate10(self):
+        array = [[]]
+        kernel = np.array([[1, 1]])
+        output = ndimage.correlate(array, kernel)
+        assert_array_almost_equal(array, output)
+        output = ndimage.convolve(array, kernel)
+        assert_array_almost_equal(array, output)
+
+    def test_correlate11(self):
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6]])
+        kernel = np.array([[1, 1],
+                           [1, 1]])
+        output = ndimage.correlate(array, kernel)
+        assert_array_almost_equal([[4, 6, 10], [10, 12, 16]], output)
+        output = ndimage.convolve(array, kernel)
+        assert_array_almost_equal([[12, 16, 18], [18, 22, 24]], output)
+
+    def test_correlate12(self):
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6]])
+        kernel = np.array([[1, 0],
+                           [0, 1]])
+        output = ndimage.correlate(array, kernel)
+        assert_array_almost_equal([[2, 3, 5], [5, 6, 8]], output)
+        output = ndimage.convolve(array, kernel)
+        assert_array_almost_equal([[6, 8, 9], [9, 11, 12]], output)
+
+    @pytest.mark.parametrize('dtype_array', types)
+    @pytest.mark.parametrize('dtype_kernel', types)
+    def test_correlate13(self, dtype_array, dtype_kernel):
+        kernel = np.array([[1, 0],
+                              [0, 1]])
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6]], dtype_array)
+        output = ndimage.correlate(array, kernel, output=dtype_kernel)
+        assert_array_almost_equal([[2, 3, 5], [5, 6, 8]], output)
+        assert_equal(output.dtype.type, dtype_kernel)
+
+        output = ndimage.convolve(array, kernel,
+                                  output=dtype_kernel)
+        assert_array_almost_equal([[6, 8, 9], [9, 11, 12]], output)
+        assert_equal(output.dtype.type, dtype_kernel)
+
+    @pytest.mark.parametrize('dtype_array', types)
+    @pytest.mark.parametrize('dtype_output', types)
+    def test_correlate14(self, dtype_array, dtype_output):
+        kernel = np.array([[1, 0],
+                           [0, 1]])
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6]], dtype_array)
+        output = np.zeros(array.shape, dtype_output)
+        ndimage.correlate(array, kernel, output=output)
+        assert_array_almost_equal([[2, 3, 5], [5, 6, 8]], output)
+        assert_equal(output.dtype.type, dtype_output)
+
+        ndimage.convolve(array, kernel, output=output)
+        assert_array_almost_equal([[6, 8, 9], [9, 11, 12]], output)
+        assert_equal(output.dtype.type, dtype_output)
+
+    @pytest.mark.parametrize('dtype_array', types)
+    def test_correlate15(self, dtype_array):
+        kernel = np.array([[1, 0],
+                           [0, 1]])
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6]], dtype_array)
+        output = ndimage.correlate(array, kernel, output=np.float32)
+        assert_array_almost_equal([[2, 3, 5], [5, 6, 8]], output)
+        assert_equal(output.dtype.type, np.float32)
+
+        output = ndimage.convolve(array, kernel, output=np.float32)
+        assert_array_almost_equal([[6, 8, 9], [9, 11, 12]], output)
+        assert_equal(output.dtype.type, np.float32)
+
+    @pytest.mark.parametrize('dtype_array', types)
+    def test_correlate16(self, dtype_array):
+        kernel = np.array([[0.5, 0],
+                           [0, 0.5]])
+        array = np.array([[1, 2, 3], [4, 5, 6]], dtype_array)
+        output = ndimage.correlate(array, kernel, output=np.float32)
+        assert_array_almost_equal([[1, 1.5, 2.5], [2.5, 3, 4]], output)
+        assert_equal(output.dtype.type, np.float32)
+
+        output = ndimage.convolve(array, kernel, output=np.float32)
+        assert_array_almost_equal([[3, 4, 4.5], [4.5, 5.5, 6]], output)
+        assert_equal(output.dtype.type, np.float32)
+
+    def test_correlate17(self):
+        array = np.array([1, 2, 3])
+        tcor = [3, 5, 6]
+        tcov = [2, 3, 5]
+        kernel = np.array([1, 1])
+        output = ndimage.correlate(array, kernel, origin=-1)
+        assert_array_almost_equal(tcor, output)
+        output = ndimage.convolve(array, kernel, origin=-1)
+        assert_array_almost_equal(tcov, output)
+        output = ndimage.correlate1d(array, kernel, origin=-1)
+        assert_array_almost_equal(tcor, output)
+        output = ndimage.convolve1d(array, kernel, origin=-1)
+        assert_array_almost_equal(tcov, output)
+
+    @pytest.mark.parametrize('dtype_array', types)
+    def test_correlate18(self, dtype_array):
+        kernel = np.array([[1, 0],
+                           [0, 1]])
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6]], dtype_array)
+        output = ndimage.correlate(array, kernel,
+                                   output=np.float32,
+                                   mode='nearest', origin=-1)
+        assert_array_almost_equal([[6, 8, 9], [9, 11, 12]], output)
+        assert_equal(output.dtype.type, np.float32)
+
+        output = ndimage.convolve(array, kernel,
+                                  output=np.float32,
+                                  mode='nearest', origin=-1)
+        assert_array_almost_equal([[2, 3, 5], [5, 6, 8]], output)
+        assert_equal(output.dtype.type, np.float32)
+
+    def test_correlate_mode_sequence(self):
+        kernel = np.ones((2, 2))
+        array = np.ones((3, 3), float)
+        with assert_raises(RuntimeError):
+            ndimage.correlate(array, kernel, mode=['nearest', 'reflect'])
+        with assert_raises(RuntimeError):
+            ndimage.convolve(array, kernel, mode=['nearest', 'reflect'])
+
+    @pytest.mark.parametrize('dtype_array', types)
+    def test_correlate19(self, dtype_array):
+        kernel = np.array([[1, 0],
+                           [0, 1]])
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6]], dtype_array)
+        output = ndimage.correlate(array, kernel,
+                                   output=np.float32,
+                                   mode='nearest', origin=[-1, 0])
+        assert_array_almost_equal([[5, 6, 8], [8, 9, 11]], output)
+        assert_equal(output.dtype.type, np.float32)
+
+        output = ndimage.convolve(array, kernel,
+                                  output=np.float32,
+                                  mode='nearest', origin=[-1, 0])
+        assert_array_almost_equal([[3, 5, 6], [6, 8, 9]], output)
+        assert_equal(output.dtype.type, np.float32)
+
+    @pytest.mark.parametrize('dtype_array', types)
+    @pytest.mark.parametrize('dtype_output', types)
+    def test_correlate20(self, dtype_array, dtype_output):
+        weights = np.array([1, 2, 1])
+        expected = [[5, 10, 15], [7, 14, 21]]
+        array = np.array([[1, 2, 3],
+                          [2, 4, 6]], dtype_array)
+        output = np.zeros((2, 3), dtype_output)
+        ndimage.correlate1d(array, weights, axis=0, output=output)
+        assert_array_almost_equal(output, expected)
+        ndimage.convolve1d(array, weights, axis=0, output=output)
+        assert_array_almost_equal(output, expected)
+
+    def test_correlate21(self):
+        array = np.array([[1, 2, 3],
+                          [2, 4, 6]])
+        expected = [[5, 10, 15], [7, 14, 21]]
+        weights = np.array([1, 2, 1])
+        output = ndimage.correlate1d(array, weights, axis=0)
+        assert_array_almost_equal(output, expected)
+        output = ndimage.convolve1d(array, weights, axis=0)
+        assert_array_almost_equal(output, expected)
+
+    @pytest.mark.parametrize('dtype_array', types)
+    @pytest.mark.parametrize('dtype_output', types)
+    def test_correlate22(self, dtype_array, dtype_output):
+        weights = np.array([1, 2, 1])
+        expected = [[6, 12, 18], [6, 12, 18]]
+        array = np.array([[1, 2, 3],
+                          [2, 4, 6]], dtype_array)
+        output = np.zeros((2, 3), dtype_output)
+        ndimage.correlate1d(array, weights, axis=0,
+                            mode='wrap', output=output)
+        assert_array_almost_equal(output, expected)
+        ndimage.convolve1d(array, weights, axis=0,
+                           mode='wrap', output=output)
+        assert_array_almost_equal(output, expected)
+
+    @pytest.mark.parametrize('dtype_array', types)
+    @pytest.mark.parametrize('dtype_output', types)
+    def test_correlate23(self, dtype_array, dtype_output):
+        weights = np.array([1, 2, 1])
+        expected = [[5, 10, 15], [7, 14, 21]]
+        array = np.array([[1, 2, 3],
+                          [2, 4, 6]], dtype_array)
+        output = np.zeros((2, 3), dtype_output)
+        ndimage.correlate1d(array, weights, axis=0,
+                            mode='nearest', output=output)
+        assert_array_almost_equal(output, expected)
+        ndimage.convolve1d(array, weights, axis=0,
+                           mode='nearest', output=output)
+        assert_array_almost_equal(output, expected)
+
+    @pytest.mark.parametrize('dtype_array', types)
+    @pytest.mark.parametrize('dtype_output', types)
+    def test_correlate24(self, dtype_array, dtype_output):
+        weights = np.array([1, 2, 1])
+        tcor = [[7, 14, 21], [8, 16, 24]]
+        tcov = [[4, 8, 12], [5, 10, 15]]
+        array = np.array([[1, 2, 3],
+                          [2, 4, 6]], dtype_array)
+        output = np.zeros((2, 3), dtype_output)
+        ndimage.correlate1d(array, weights, axis=0,
+                            mode='nearest', output=output, origin=-1)
+        assert_array_almost_equal(output, tcor)
+        ndimage.convolve1d(array, weights, axis=0,
+                           mode='nearest', output=output, origin=-1)
+        assert_array_almost_equal(output, tcov)
+
+    @pytest.mark.parametrize('dtype_array', types)
+    @pytest.mark.parametrize('dtype_output', types)
+    def test_correlate25(self, dtype_array, dtype_output):
+        weights = np.array([1, 2, 1])
+        tcor = [[4, 8, 12], [5, 10, 15]]
+        tcov = [[7, 14, 21], [8, 16, 24]]
+        array = np.array([[1, 2, 3],
+                          [2, 4, 6]], dtype_array)
+        output = np.zeros((2, 3), dtype_output)
+        ndimage.correlate1d(array, weights, axis=0,
+                            mode='nearest', output=output, origin=1)
+        assert_array_almost_equal(output, tcor)
+        ndimage.convolve1d(array, weights, axis=0,
+                           mode='nearest', output=output, origin=1)
+        assert_array_almost_equal(output, tcov)
+
+    def test_correlate26(self):
+        # test fix for gh-11661 (mirror extension of a length 1 signal)
+        y = ndimage.convolve1d(np.ones(1), np.ones(5), mode='mirror')
+        assert_array_equal(y, np.array(5.))
+
+        y = ndimage.correlate1d(np.ones(1), np.ones(5), mode='mirror')
+        assert_array_equal(y, np.array(5.))
+
+    @pytest.mark.parametrize('dtype_kernel', complex_types)
+    @pytest.mark.parametrize('dtype_input', types)
+    @pytest.mark.parametrize('dtype_output', complex_types)
+    def test_correlate_complex_kernel(self, dtype_input, dtype_kernel,
+                                      dtype_output):
+        kernel = np.array([[1, 0],
+                           [0, 1 + 1j]], dtype_kernel)
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6]], dtype_input)
+        self._validate_complex(array, kernel, dtype_output)
+
+    @pytest.mark.parametrize('dtype_kernel', complex_types)
+    @pytest.mark.parametrize('dtype_input', types)
+    @pytest.mark.parametrize('dtype_output', complex_types)
+    @pytest.mark.parametrize('mode', ['grid-constant', 'constant'])
+    def test_correlate_complex_kernel_cval(self, dtype_input, dtype_kernel,
+                                           dtype_output, mode):
+        # test use of non-zero cval with complex inputs
+        # also verifies that mode 'grid-constant' does not segfault
+        kernel = np.array([[1, 0],
+                           [0, 1 + 1j]], dtype_kernel)
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6]], dtype_input)
+        self._validate_complex(array, kernel, dtype_output, mode=mode,
+                               cval=5.0)
+
+    @pytest.mark.parametrize('dtype_kernel', complex_types)
+    @pytest.mark.parametrize('dtype_input', types)
+    def test_correlate_complex_kernel_invalid_cval(self, dtype_input,
+                                                   dtype_kernel):
+        # cannot give complex cval with a real image
+        kernel = np.array([[1, 0],
+                           [0, 1 + 1j]], dtype_kernel)
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6]], dtype_input)
+        for func in [ndimage.convolve, ndimage.correlate, ndimage.convolve1d,
+                     ndimage.correlate1d]:
+            with pytest.raises(ValueError):
+                func(array, kernel, mode='constant', cval=5.0 + 1.0j,
+                     output=np.complex64)
+
+    @pytest.mark.parametrize('dtype_kernel', complex_types)
+    @pytest.mark.parametrize('dtype_input', types)
+    @pytest.mark.parametrize('dtype_output', complex_types)
+    def test_correlate1d_complex_kernel(self, dtype_input, dtype_kernel,
+                                        dtype_output):
+        kernel = np.array([1, 1 + 1j], dtype_kernel)
+        array = np.array([1, 2, 3, 4, 5, 6], dtype_input)
+        self._validate_complex(array, kernel, dtype_output)
+
+    @pytest.mark.parametrize('dtype_kernel', complex_types)
+    @pytest.mark.parametrize('dtype_input', types)
+    @pytest.mark.parametrize('dtype_output', complex_types)
+    def test_correlate1d_complex_kernel_cval(self, dtype_input, dtype_kernel,
+                                             dtype_output):
+        kernel = np.array([1, 1 + 1j], dtype_kernel)
+        array = np.array([1, 2, 3, 4, 5, 6], dtype_input)
+        self._validate_complex(array, kernel, dtype_output, mode='constant',
+                               cval=5.0)
+
+    @pytest.mark.parametrize('dtype_kernel', types)
+    @pytest.mark.parametrize('dtype_input', complex_types)
+    @pytest.mark.parametrize('dtype_output', complex_types)
+    def test_correlate_complex_input(self, dtype_input, dtype_kernel,
+                                     dtype_output):
+        kernel = np.array([[1, 0],
+                           [0, 1]], dtype_kernel)
+        array = np.array([[1, 2j, 3],
+                          [1 + 4j, 5, 6j]], dtype_input)
+        self._validate_complex(array, kernel, dtype_output)
+
+    @pytest.mark.parametrize('dtype_kernel', types)
+    @pytest.mark.parametrize('dtype_input', complex_types)
+    @pytest.mark.parametrize('dtype_output', complex_types)
+    def test_correlate1d_complex_input(self, dtype_input, dtype_kernel,
+                                       dtype_output):
+        kernel = np.array([1, 0, 1], dtype_kernel)
+        array = np.array([1, 2j, 3, 1 + 4j, 5, 6j], dtype_input)
+        self._validate_complex(array, kernel, dtype_output)
+
+    @pytest.mark.parametrize('dtype_kernel', types)
+    @pytest.mark.parametrize('dtype_input', complex_types)
+    @pytest.mark.parametrize('dtype_output', complex_types)
+    def test_correlate1d_complex_input_cval(self, dtype_input, dtype_kernel,
+                                            dtype_output):
+        kernel = np.array([1, 0, 1], dtype_kernel)
+        array = np.array([1, 2j, 3, 1 + 4j, 5, 6j], dtype_input)
+        self._validate_complex(array, kernel, dtype_output, mode='constant',
+                               cval=5 - 3j)
+
+    @pytest.mark.parametrize('dtype', complex_types)
+    @pytest.mark.parametrize('dtype_output', complex_types)
+    def test_correlate_complex_input_and_kernel(self, dtype, dtype_output):
+        kernel = np.array([[1, 0],
+                           [0, 1 + 1j]], dtype)
+        array = np.array([[1, 2j, 3],
+                          [1 + 4j, 5, 6j]], dtype)
+        self._validate_complex(array, kernel, dtype_output)
+
+    @pytest.mark.parametrize('dtype', complex_types)
+    @pytest.mark.parametrize('dtype_output', complex_types)
+    def test_correlate_complex_input_and_kernel_cval(self, dtype,
+                                                     dtype_output):
+        kernel = np.array([[1, 0],
+                           [0, 1 + 1j]], dtype)
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6]], dtype)
+        self._validate_complex(array, kernel, dtype_output, mode='constant',
+                               cval=5.0 + 2.0j)
+
+    @pytest.mark.parametrize('dtype', complex_types)
+    @pytest.mark.parametrize('dtype_output', complex_types)
+    def test_correlate1d_complex_input_and_kernel(self, dtype, dtype_output):
+        kernel = np.array([1, 1 + 1j], dtype)
+        array = np.array([1, 2j, 3, 1 + 4j, 5, 6j], dtype)
+        self._validate_complex(array, kernel, dtype_output)
+
+    @pytest.mark.parametrize('dtype', complex_types)
+    @pytest.mark.parametrize('dtype_output', complex_types)
+    def test_correlate1d_complex_input_and_kernel_cval(self, dtype,
+                                                       dtype_output):
+        kernel = np.array([1, 1 + 1j], dtype)
+        array = np.array([1, 2j, 3, 1 + 4j, 5, 6j], dtype)
+        self._validate_complex(array, kernel, dtype_output, mode='constant',
+                               cval=5.0 + 2.0j)
+
+    def test_gauss01(self):
+        input = np.array([[1, 2, 3],
+                          [2, 4, 6]], np.float32)
+        output = ndimage.gaussian_filter(input, 0)
+        assert_array_almost_equal(output, input)
+
+    def test_gauss02(self):
+        input = np.array([[1, 2, 3],
+                          [2, 4, 6]], np.float32)
+        output = ndimage.gaussian_filter(input, 1.0)
+        assert_equal(input.dtype, output.dtype)
+        assert_equal(input.shape, output.shape)
+
+    def test_gauss03(self):
+        # single precision data
+        input = np.arange(100 * 100).astype(np.float32)
+        input.shape = (100, 100)
+        output = ndimage.gaussian_filter(input, [1.0, 1.0])
+
+        assert_equal(input.dtype, output.dtype)
+        assert_equal(input.shape, output.shape)
+
+        # input.sum() is 49995000.0.  With single precision floats, we can't
+        # expect more than 8 digits of accuracy, so use decimal=0 in this test.
+        assert_almost_equal(output.sum(dtype='d'), input.sum(dtype='d'),
+                            decimal=0)
+        assert_(sumsq(input, output) > 1.0)
+
+    def test_gauss04(self):
+        input = np.arange(100 * 100).astype(np.float32)
+        input.shape = (100, 100)
+        otype = np.float64
+        output = ndimage.gaussian_filter(input, [1.0, 1.0], output=otype)
+        assert_equal(output.dtype.type, np.float64)
+        assert_equal(input.shape, output.shape)
+        assert_(sumsq(input, output) > 1.0)
+
+    def test_gauss05(self):
+        input = np.arange(100 * 100).astype(np.float32)
+        input.shape = (100, 100)
+        otype = np.float64
+        output = ndimage.gaussian_filter(input, [1.0, 1.0],
+                                         order=1, output=otype)
+        assert_equal(output.dtype.type, np.float64)
+        assert_equal(input.shape, output.shape)
+        assert_(sumsq(input, output) > 1.0)
+
+    def test_gauss06(self):
+        input = np.arange(100 * 100).astype(np.float32)
+        input.shape = (100, 100)
+        otype = np.float64
+        output1 = ndimage.gaussian_filter(input, [1.0, 1.0], output=otype)
+        output2 = ndimage.gaussian_filter(input, 1.0, output=otype)
+        assert_array_almost_equal(output1, output2)
+
+    def test_gauss_memory_overlap(self):
+        input = np.arange(100 * 100).astype(np.float32)
+        input.shape = (100, 100)
+        output1 = ndimage.gaussian_filter(input, 1.0)
+        ndimage.gaussian_filter(input, 1.0, output=input)
+        assert_array_almost_equal(output1, input)
+
+    @pytest.mark.parametrize(('filter_func', 'extra_args', 'size0', 'size'),
+                             [(ndimage.gaussian_filter, (), 0, 1.0),
+                              (ndimage.uniform_filter, (), 1, 3),
+                              (ndimage.minimum_filter, (), 1, 3),
+                              (ndimage.maximum_filter, (), 1, 3),
+                              (ndimage.median_filter, (), 1, 3),
+                              (ndimage.rank_filter, (1,), 1, 3),
+                              (ndimage.percentile_filter, (40,), 1, 3)])
+    @pytest.mark.parametrize(
+        'axes',
+        tuple(itertools.combinations(range(-3, 3), 1))
+        + tuple(itertools.combinations(range(-3, 3), 2))
+        + ((0, 1, 2),))
+    def test_filter_axes(self, filter_func, extra_args, size0, size, axes):
+        # Note: `size` is called `sigma` in `gaussian_filter`
+        array = np.arange(6 * 8 * 12, dtype=np.float64).reshape(6, 8, 12)
+        axes = np.array(axes)
+
+        if len(set(axes % array.ndim)) != len(axes):
+            # parametrized cases with duplicate axes raise an error
+            with pytest.raises(ValueError, match="axes must be unique"):
+                filter_func(array, *extra_args, size, axes=axes)
+            return
+        output = filter_func(array, *extra_args, size, axes=axes)
+
+        # result should be equivalent to sigma=0.0/size=1 on unfiltered axes
+        all_sizes = (size if ax in (axes % array.ndim) else size0
+                     for ax in range(array.ndim))
+        expected = filter_func(array, *extra_args, all_sizes)
+        assert_allclose(output, expected)
+
+    kwargs_gauss = dict(radius=[4, 2, 3], order=[0, 1, 2],
+                        mode=['reflect', 'nearest', 'constant'])
+    kwargs_other = dict(origin=(-1, 0, 1),
+                        mode=['reflect', 'nearest', 'constant'])
+    kwargs_rank = dict(origin=(-1, 0, 1))
+
+    @pytest.mark.parametrize("filter_func, size0, size, kwargs",
+                             [(ndimage.gaussian_filter, 0, 1.0, kwargs_gauss),
+                              (ndimage.uniform_filter, 1, 3, kwargs_other),
+                              (ndimage.maximum_filter, 1, 3, kwargs_other),
+                              (ndimage.minimum_filter, 1, 3, kwargs_other),
+                              (ndimage.median_filter, 1, 3, kwargs_rank),
+                              (ndimage.rank_filter, 1, 3, kwargs_rank),
+                              (ndimage.percentile_filter, 1, 3, kwargs_rank)])
+    @pytest.mark.parametrize('axes', itertools.combinations(range(-3, 3), 2))
+    def test_filter_axes_kwargs(self, filter_func, size0, size, kwargs, axes):
+        array = np.arange(6 * 8 * 12, dtype=np.float64).reshape(6, 8, 12)
+
+        kwargs = {key: np.array(val) for key, val in kwargs.items()}
+        axes = np.array(axes)
+        n_axes = axes.size
+
+        if filter_func == ndimage.rank_filter:
+            args = (2,)  # (rank,)
+        elif filter_func == ndimage.percentile_filter:
+            args = (30,)  # (percentile,)
+        else:
+            args = ()
+
+        # form kwargs that specify only the axes in `axes`
+        reduced_kwargs = {key: val[axes] for key, val in kwargs.items()}
+        if len(set(axes % array.ndim)) != len(axes):
+            # parametrized cases with duplicate axes raise an error
+            with pytest.raises(ValueError, match="axes must be unique"):
+                filter_func(array, *args, [size]*n_axes, axes=axes,
+                            **reduced_kwargs)
+            return
+
+        output = filter_func(array, *args, [size]*n_axes, axes=axes,
+                             **reduced_kwargs)
+
+        # result should be equivalent to sigma=0.0/size=1 on unfiltered axes
+        size_3d = np.full(array.ndim, fill_value=size0)
+        size_3d[axes] = size
+        if 'origin' in kwargs:
+            # origin should be zero on the axis that has size 0
+            origin = np.array([0, 0, 0])
+            origin[axes] = reduced_kwargs['origin']
+            kwargs['origin'] = origin
+        expected = filter_func(array, *args, size_3d, **kwargs)
+        assert_allclose(output, expected)
+
+    @pytest.mark.parametrize("filter_func, kwargs",
+                             [(ndimage.minimum_filter, {}),
+                              (ndimage.maximum_filter, {}),
+                              (ndimage.median_filter, {}),
+                              (ndimage.rank_filter, {"rank": 1}),
+                              (ndimage.percentile_filter, {"percentile": 30})])
+    def test_filter_weights_subset_axes_origins(self, filter_func, kwargs):
+        axes = (-2, -1)
+        origins = (0, 1)
+        array = np.arange(6 * 8 * 12, dtype=np.float64).reshape(6, 8, 12)
+        axes = np.array(axes)
+
+        # weights with ndim matching len(axes)
+        footprint = np.ones((3, 5), dtype=bool)
+        footprint[0, 1] = 0  # make non-separable
+
+        output = filter_func(
+            array, footprint=footprint, axes=axes, origin=origins, **kwargs)
+
+        output0 = filter_func(
+            array, footprint=footprint, axes=axes, origin=0, **kwargs)
+
+        # output has origin shift on last axis relative to output0, so
+        # expect shifted arrays to be equal.
+        np.testing.assert_array_equal(output[:, :, 1:], output0[:, :, :-1])
+
+    @pytest.mark.parametrize(
+        'filter_func, args',
+        [(ndimage.gaussian_filter, (1.0,)),      # args = (sigma,)
+         (ndimage.uniform_filter, (3,)),         # args = (size,)
+         (ndimage.minimum_filter, (3,)),         # args = (size,)
+         (ndimage.maximum_filter, (3,)),         # args = (size,)
+         (ndimage.median_filter, (3,)),          # args = (size,)
+         (ndimage.rank_filter, (2, 3)),          # args = (rank, size)
+         (ndimage.percentile_filter, (30, 3))])  # args = (percentile, size)
+    @pytest.mark.parametrize(
+        'axes', [(1.5,), (0, 1, 2, 3), (3,), (-4,)]
+    )
+    def test_filter_invalid_axes(self, filter_func, args, axes):
+        array = np.arange(6 * 8 * 12, dtype=np.float64).reshape(6, 8, 12)
+        if any(isinstance(ax, float) for ax in axes):
+            error_class = TypeError
+            match = "cannot be interpreted as an integer"
+        else:
+            error_class = ValueError
+            match = "out of range"
+        with pytest.raises(error_class, match=match):
+            filter_func(array, *args, axes=axes)
+
+    @pytest.mark.parametrize(
+        'filter_func, kwargs',
+        [(ndimage.minimum_filter, {}),
+         (ndimage.maximum_filter, {}),
+         (ndimage.median_filter, {}),
+         (ndimage.rank_filter, dict(rank=3)),
+         (ndimage.percentile_filter, dict(percentile=30))])
+    @pytest.mark.parametrize(
+        'axes', [(0, ), (1, 2), (0, 1, 2)]
+    )
+    @pytest.mark.parametrize('separable_footprint', [False, True])
+    def test_filter_invalid_footprint_ndim(self, filter_func, kwargs, axes,
+                                           separable_footprint):
+        array = np.arange(6 * 8 * 12, dtype=np.float64).reshape(6, 8, 12)
+        # create a footprint with one too many dimensions
+        footprint = np.ones((3,) * (len(axes) + 1))
+        if not separable_footprint:
+            footprint[(0,) * footprint.ndim] = 0
+        if (filter_func in [ndimage.minimum_filter, ndimage.maximum_filter]
+            and separable_footprint):
+            match = "sequence argument must have length equal to input rank"
+        else:
+            match = "footprint array has incorrect shape"
+        with pytest.raises(RuntimeError, match=match):
+            filter_func(array, **kwargs, footprint=footprint, axes=axes)
+
+    @pytest.mark.parametrize('n_mismatch', [1, 3])
+    @pytest.mark.parametrize('filter_func, kwargs, key, val',
+                             _cases_axes_tuple_length_mismatch())
+    def test_filter_tuple_length_mismatch(self, n_mismatch, filter_func,
+                                          kwargs, key, val):
+        # Test for the intended RuntimeError when a kwargs has an invalid size
+        array = np.arange(6 * 8 * 12, dtype=np.float64).reshape(6, 8, 12)
+        kwargs = dict(**kwargs, axes=(0, 1))
+        kwargs[key] = (val,) * n_mismatch
+        err_msg = "sequence argument must have length equal to input rank"
+        with pytest.raises(RuntimeError, match=err_msg):
+            filter_func(array, **kwargs)
+
+    @pytest.mark.parametrize('dtype', types + complex_types)
+    def test_prewitt01(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        t = ndimage.correlate1d(array, [-1.0, 0.0, 1.0], 0)
+        t = ndimage.correlate1d(t, [1.0, 1.0, 1.0], 1)
+        output = ndimage.prewitt(array, 0)
+        assert_array_almost_equal(t, output)
+
+    @pytest.mark.parametrize('dtype', types + complex_types)
+    def test_prewitt02(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        t = ndimage.correlate1d(array, [-1.0, 0.0, 1.0], 0)
+        t = ndimage.correlate1d(t, [1.0, 1.0, 1.0], 1)
+        output = np.zeros(array.shape, dtype)
+        ndimage.prewitt(array, 0, output)
+        assert_array_almost_equal(t, output)
+
+    @pytest.mark.parametrize('dtype', types + complex_types)
+    def test_prewitt03(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        t = ndimage.correlate1d(array, [-1.0, 0.0, 1.0], 1)
+        t = ndimage.correlate1d(t, [1.0, 1.0, 1.0], 0)
+        output = ndimage.prewitt(array, 1)
+        assert_array_almost_equal(t, output)
+
+    @pytest.mark.parametrize('dtype', types + complex_types)
+    def test_prewitt04(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        t = ndimage.prewitt(array, -1)
+        output = ndimage.prewitt(array, 1)
+        assert_array_almost_equal(t, output)
+
+    @pytest.mark.parametrize('dtype', types + complex_types)
+    def test_sobel01(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        t = ndimage.correlate1d(array, [-1.0, 0.0, 1.0], 0)
+        t = ndimage.correlate1d(t, [1.0, 2.0, 1.0], 1)
+        output = ndimage.sobel(array, 0)
+        assert_array_almost_equal(t, output)
+
+    @pytest.mark.parametrize('dtype', types + complex_types)
+    def test_sobel02(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        t = ndimage.correlate1d(array, [-1.0, 0.0, 1.0], 0)
+        t = ndimage.correlate1d(t, [1.0, 2.0, 1.0], 1)
+        output = np.zeros(array.shape, dtype)
+        ndimage.sobel(array, 0, output)
+        assert_array_almost_equal(t, output)
+
+    @pytest.mark.parametrize('dtype', types + complex_types)
+    def test_sobel03(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        t = ndimage.correlate1d(array, [-1.0, 0.0, 1.0], 1)
+        t = ndimage.correlate1d(t, [1.0, 2.0, 1.0], 0)
+        output = np.zeros(array.shape, dtype)
+        output = ndimage.sobel(array, 1)
+        assert_array_almost_equal(t, output)
+
+    @pytest.mark.parametrize('dtype', types + complex_types)
+    def test_sobel04(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        t = ndimage.sobel(array, -1)
+        output = ndimage.sobel(array, 1)
+        assert_array_almost_equal(t, output)
+
+    @pytest.mark.parametrize('dtype',
+                             [np.int32, np.float32, np.float64,
+                              np.complex64, np.complex128])
+    def test_laplace01(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype) * 100
+        tmp1 = ndimage.correlate1d(array, [1, -2, 1], 0)
+        tmp2 = ndimage.correlate1d(array, [1, -2, 1], 1)
+        output = ndimage.laplace(array)
+        assert_array_almost_equal(tmp1 + tmp2, output)
+
+    @pytest.mark.parametrize('dtype',
+                             [np.int32, np.float32, np.float64,
+                              np.complex64, np.complex128])
+    def test_laplace02(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype) * 100
+        tmp1 = ndimage.correlate1d(array, [1, -2, 1], 0)
+        tmp2 = ndimage.correlate1d(array, [1, -2, 1], 1)
+        output = np.zeros(array.shape, dtype)
+        ndimage.laplace(array, output=output)
+        assert_array_almost_equal(tmp1 + tmp2, output)
+
+    @pytest.mark.parametrize('dtype',
+                             [np.int32, np.float32, np.float64,
+                              np.complex64, np.complex128])
+    def test_gaussian_laplace01(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype) * 100
+        tmp1 = ndimage.gaussian_filter(array, 1.0, [2, 0])
+        tmp2 = ndimage.gaussian_filter(array, 1.0, [0, 2])
+        output = ndimage.gaussian_laplace(array, 1.0)
+        assert_array_almost_equal(tmp1 + tmp2, output)
+
+    @pytest.mark.parametrize('dtype',
+                             [np.int32, np.float32, np.float64,
+                              np.complex64, np.complex128])
+    def test_gaussian_laplace02(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype) * 100
+        tmp1 = ndimage.gaussian_filter(array, 1.0, [2, 0])
+        tmp2 = ndimage.gaussian_filter(array, 1.0, [0, 2])
+        output = np.zeros(array.shape, dtype)
+        ndimage.gaussian_laplace(array, 1.0, output)
+        assert_array_almost_equal(tmp1 + tmp2, output)
+
+    @pytest.mark.parametrize('dtype', types + complex_types)
+    def test_generic_laplace01(self, dtype):
+        def derivative2(input, axis, output, mode, cval, a, b):
+            sigma = [a, b / 2.0]
+            input = np.asarray(input)
+            order = [0] * input.ndim
+            order[axis] = 2
+            return ndimage.gaussian_filter(input, sigma, order,
+                                           output, mode, cval)
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        output = np.zeros(array.shape, dtype)
+        tmp = ndimage.generic_laplace(array, derivative2,
+                                      extra_arguments=(1.0,),
+                                      extra_keywords={'b': 2.0})
+        ndimage.gaussian_laplace(array, 1.0, output)
+        assert_array_almost_equal(tmp, output)
+
+    @pytest.mark.parametrize('dtype',
+                             [np.int32, np.float32, np.float64,
+                              np.complex64, np.complex128])
+    def test_gaussian_gradient_magnitude01(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype) * 100
+        tmp1 = ndimage.gaussian_filter(array, 1.0, [1, 0])
+        tmp2 = ndimage.gaussian_filter(array, 1.0, [0, 1])
+        output = ndimage.gaussian_gradient_magnitude(array, 1.0)
+        expected = tmp1 * tmp1 + tmp2 * tmp2
+        expected = np.sqrt(expected).astype(dtype)
+        assert_array_almost_equal(expected, output)
+
+    @pytest.mark.parametrize('dtype',
+                             [np.int32, np.float32, np.float64,
+                              np.complex64, np.complex128])
+    def test_gaussian_gradient_magnitude02(self, dtype):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype) * 100
+        tmp1 = ndimage.gaussian_filter(array, 1.0, [1, 0])
+        tmp2 = ndimage.gaussian_filter(array, 1.0, [0, 1])
+        output = np.zeros(array.shape, dtype)
+        ndimage.gaussian_gradient_magnitude(array, 1.0, output)
+        expected = tmp1 * tmp1 + tmp2 * tmp2
+        expected = np.sqrt(expected).astype(dtype)
+        assert_array_almost_equal(expected, output)
+
+    def test_generic_gradient_magnitude01(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], np.float64)
+
+        def derivative(input, axis, output, mode, cval, a, b):
+            sigma = [a, b / 2.0]
+            input = np.asarray(input)
+            order = [0] * input.ndim
+            order[axis] = 1
+            return ndimage.gaussian_filter(input, sigma, order,
+                                           output, mode, cval)
+        tmp1 = ndimage.gaussian_gradient_magnitude(array, 1.0)
+        tmp2 = ndimage.generic_gradient_magnitude(
+            array, derivative, extra_arguments=(1.0,),
+            extra_keywords={'b': 2.0})
+        assert_array_almost_equal(tmp1, tmp2)
+
+    def test_uniform01(self):
+        array = np.array([2, 4, 6])
+        size = 2
+        output = ndimage.uniform_filter1d(array, size, origin=-1)
+        assert_array_almost_equal([3, 5, 6], output)
+
+    def test_uniform01_complex(self):
+        array = np.array([2 + 1j, 4 + 2j, 6 + 3j], dtype=np.complex128)
+        size = 2
+        output = ndimage.uniform_filter1d(array, size, origin=-1)
+        assert_array_almost_equal([3, 5, 6], output.real)
+        assert_array_almost_equal([1.5, 2.5, 3], output.imag)
+
+    def test_uniform02(self):
+        array = np.array([1, 2, 3])
+        filter_shape = [0]
+        output = ndimage.uniform_filter(array, filter_shape)
+        assert_array_almost_equal(array, output)
+
+    def test_uniform03(self):
+        array = np.array([1, 2, 3])
+        filter_shape = [1]
+        output = ndimage.uniform_filter(array, filter_shape)
+        assert_array_almost_equal(array, output)
+
+    def test_uniform04(self):
+        array = np.array([2, 4, 6])
+        filter_shape = [2]
+        output = ndimage.uniform_filter(array, filter_shape)
+        assert_array_almost_equal([2, 3, 5], output)
+
+    def test_uniform05(self):
+        array = []
+        filter_shape = [1]
+        output = ndimage.uniform_filter(array, filter_shape)
+        assert_array_almost_equal([], output)
+
+    @pytest.mark.parametrize('dtype_array', types)
+    @pytest.mark.parametrize('dtype_output', types)
+    def test_uniform06(self, dtype_array, dtype_output):
+        filter_shape = [2, 2]
+        array = np.array([[4, 8, 12],
+                          [16, 20, 24]], dtype_array)
+        output = ndimage.uniform_filter(
+            array, filter_shape, output=dtype_output)
+        assert_array_almost_equal([[4, 6, 10], [10, 12, 16]], output)
+        assert_equal(output.dtype.type, dtype_output)
+
+    @pytest.mark.parametrize('dtype_array', complex_types)
+    @pytest.mark.parametrize('dtype_output', complex_types)
+    def test_uniform06_complex(self, dtype_array, dtype_output):
+        filter_shape = [2, 2]
+        array = np.array([[4, 8 + 5j, 12],
+                          [16, 20, 24]], dtype_array)
+        output = ndimage.uniform_filter(
+            array, filter_shape, output=dtype_output)
+        assert_array_almost_equal([[4, 6, 10], [10, 12, 16]], output.real)
+        assert_equal(output.dtype.type, dtype_output)
+
+    def test_minimum_filter01(self):
+        array = np.array([1, 2, 3, 4, 5])
+        filter_shape = np.array([2])
+        output = ndimage.minimum_filter(array, filter_shape)
+        assert_array_almost_equal([1, 1, 2, 3, 4], output)
+
+    def test_minimum_filter02(self):
+        array = np.array([1, 2, 3, 4, 5])
+        filter_shape = np.array([3])
+        output = ndimage.minimum_filter(array, filter_shape)
+        assert_array_almost_equal([1, 1, 2, 3, 4], output)
+
+    def test_minimum_filter03(self):
+        array = np.array([3, 2, 5, 1, 4])
+        filter_shape = np.array([2])
+        output = ndimage.minimum_filter(array, filter_shape)
+        assert_array_almost_equal([3, 2, 2, 1, 1], output)
+
+    def test_minimum_filter04(self):
+        array = np.array([3, 2, 5, 1, 4])
+        filter_shape = np.array([3])
+        output = ndimage.minimum_filter(array, filter_shape)
+        assert_array_almost_equal([2, 2, 1, 1, 1], output)
+
+    def test_minimum_filter05(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        filter_shape = np.array([2, 3])
+        output = ndimage.minimum_filter(array, filter_shape)
+        assert_array_almost_equal([[2, 2, 1, 1, 1],
+                                   [2, 2, 1, 1, 1],
+                                   [5, 3, 3, 1, 1]], output)
+
+    def test_minimum_filter05_overlap(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        filter_shape = np.array([2, 3])
+        ndimage.minimum_filter(array, filter_shape, output=array)
+        assert_array_almost_equal([[2, 2, 1, 1, 1],
+                                   [2, 2, 1, 1, 1],
+                                   [5, 3, 3, 1, 1]], array)
+
+    def test_minimum_filter06(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 1, 1], [1, 1, 1]]
+        output = ndimage.minimum_filter(array, footprint=footprint)
+        assert_array_almost_equal([[2, 2, 1, 1, 1],
+                                   [2, 2, 1, 1, 1],
+                                   [5, 3, 3, 1, 1]], output)
+        # separable footprint should allow mode sequence
+        output2 = ndimage.minimum_filter(array, footprint=footprint,
+                                         mode=['reflect', 'reflect'])
+        assert_array_almost_equal(output2, output)
+
+    def test_minimum_filter07(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        output = ndimage.minimum_filter(array, footprint=footprint)
+        assert_array_almost_equal([[2, 2, 1, 1, 1],
+                                   [2, 3, 1, 3, 1],
+                                   [5, 5, 3, 3, 1]], output)
+        with assert_raises(RuntimeError):
+            ndimage.minimum_filter(array, footprint=footprint,
+                                   mode=['reflect', 'constant'])
+
+    def test_minimum_filter08(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        output = ndimage.minimum_filter(array, footprint=footprint, origin=-1)
+        assert_array_almost_equal([[3, 1, 3, 1, 1],
+                                   [5, 3, 3, 1, 1],
+                                   [3, 3, 1, 1, 1]], output)
+
+    def test_minimum_filter09(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        output = ndimage.minimum_filter(array, footprint=footprint,
+                                        origin=[-1, 0])
+        assert_array_almost_equal([[2, 3, 1, 3, 1],
+                                   [5, 5, 3, 3, 1],
+                                   [5, 3, 3, 1, 1]], output)
+
+    def test_maximum_filter01(self):
+        array = np.array([1, 2, 3, 4, 5])
+        filter_shape = np.array([2])
+        output = ndimage.maximum_filter(array, filter_shape)
+        assert_array_almost_equal([1, 2, 3, 4, 5], output)
+
+    def test_maximum_filter02(self):
+        array = np.array([1, 2, 3, 4, 5])
+        filter_shape = np.array([3])
+        output = ndimage.maximum_filter(array, filter_shape)
+        assert_array_almost_equal([2, 3, 4, 5, 5], output)
+
+    def test_maximum_filter03(self):
+        array = np.array([3, 2, 5, 1, 4])
+        filter_shape = np.array([2])
+        output = ndimage.maximum_filter(array, filter_shape)
+        assert_array_almost_equal([3, 3, 5, 5, 4], output)
+
+    def test_maximum_filter04(self):
+        array = np.array([3, 2, 5, 1, 4])
+        filter_shape = np.array([3])
+        output = ndimage.maximum_filter(array, filter_shape)
+        assert_array_almost_equal([3, 5, 5, 5, 4], output)
+
+    def test_maximum_filter05(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        filter_shape = np.array([2, 3])
+        output = ndimage.maximum_filter(array, filter_shape)
+        assert_array_almost_equal([[3, 5, 5, 5, 4],
+                                   [7, 9, 9, 9, 5],
+                                   [8, 9, 9, 9, 7]], output)
+
+    def test_maximum_filter06(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 1, 1], [1, 1, 1]]
+        output = ndimage.maximum_filter(array, footprint=footprint)
+        assert_array_almost_equal([[3, 5, 5, 5, 4],
+                                   [7, 9, 9, 9, 5],
+                                   [8, 9, 9, 9, 7]], output)
+        # separable footprint should allow mode sequence
+        output2 = ndimage.maximum_filter(array, footprint=footprint,
+                                         mode=['reflect', 'reflect'])
+        assert_array_almost_equal(output2, output)
+
+    def test_maximum_filter07(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        output = ndimage.maximum_filter(array, footprint=footprint)
+        assert_array_almost_equal([[3, 5, 5, 5, 4],
+                                   [7, 7, 9, 9, 5],
+                                   [7, 9, 8, 9, 7]], output)
+        # non-separable footprint should not allow mode sequence
+        with assert_raises(RuntimeError):
+            ndimage.maximum_filter(array, footprint=footprint,
+                                   mode=['reflect', 'reflect'])
+
+    def test_maximum_filter08(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        output = ndimage.maximum_filter(array, footprint=footprint, origin=-1)
+        assert_array_almost_equal([[7, 9, 9, 5, 5],
+                                   [9, 8, 9, 7, 5],
+                                   [8, 8, 7, 7, 7]], output)
+
+    def test_maximum_filter09(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        output = ndimage.maximum_filter(array, footprint=footprint,
+                                        origin=[-1, 0])
+        assert_array_almost_equal([[7, 7, 9, 9, 5],
+                                   [7, 9, 8, 9, 7],
+                                   [8, 8, 8, 7, 7]], output)
+
+    @pytest.mark.parametrize(
+        'axes', tuple(itertools.combinations(range(-3, 3), 2))
+    )
+    @pytest.mark.parametrize(
+        'filter_func, kwargs',
+        [(ndimage.minimum_filter, {}),
+         (ndimage.maximum_filter, {}),
+         (ndimage.median_filter, {}),
+         (ndimage.rank_filter, dict(rank=3)),
+         (ndimage.percentile_filter, dict(percentile=60))]
+    )
+    def test_minmax_nonseparable_axes(self, filter_func, axes, kwargs):
+        array = np.arange(6 * 8 * 12, dtype=np.float32).reshape(6, 8, 12)
+        # use 2D triangular footprint because it is non-separable
+        footprint = np.tri(5)
+        axes = np.array(axes)
+
+        if len(set(axes % array.ndim)) != len(axes):
+            # parametrized cases with duplicate axes raise an error
+            with pytest.raises(ValueError):
+                filter_func(array, footprint=footprint, axes=axes, **kwargs)
+            return
+        output = filter_func(array, footprint=footprint, axes=axes, **kwargs)
+
+        missing_axis = tuple(set(range(3)) - set(axes % array.ndim))[0]
+        footprint_3d = np.expand_dims(footprint, missing_axis)
+        expected = filter_func(array, footprint=footprint_3d, **kwargs)
+        assert_allclose(output, expected)
+
+    def test_rank01(self):
+        array = np.array([1, 2, 3, 4, 5])
+        output = ndimage.rank_filter(array, 1, size=2)
+        assert_array_almost_equal(array, output)
+        output = ndimage.percentile_filter(array, 100, size=2)
+        assert_array_almost_equal(array, output)
+        output = ndimage.median_filter(array, 2)
+        assert_array_almost_equal(array, output)
+
+    def test_rank02(self):
+        array = np.array([1, 2, 3, 4, 5])
+        output = ndimage.rank_filter(array, 1, size=[3])
+        assert_array_almost_equal(array, output)
+        output = ndimage.percentile_filter(array, 50, size=3)
+        assert_array_almost_equal(array, output)
+        output = ndimage.median_filter(array, (3,))
+        assert_array_almost_equal(array, output)
+
+    def test_rank03(self):
+        array = np.array([3, 2, 5, 1, 4])
+        output = ndimage.rank_filter(array, 1, size=[2])
+        assert_array_almost_equal([3, 3, 5, 5, 4], output)
+        output = ndimage.percentile_filter(array, 100, size=2)
+        assert_array_almost_equal([3, 3, 5, 5, 4], output)
+
+    def test_rank04(self):
+        array = np.array([3, 2, 5, 1, 4])
+        expected = [3, 3, 2, 4, 4]
+        output = ndimage.rank_filter(array, 1, size=3)
+        assert_array_almost_equal(expected, output)
+        output = ndimage.percentile_filter(array, 50, size=3)
+        assert_array_almost_equal(expected, output)
+        output = ndimage.median_filter(array, size=3)
+        assert_array_almost_equal(expected, output)
+
+    def test_rank05(self):
+        array = np.array([3, 2, 5, 1, 4])
+        expected = [3, 3, 2, 4, 4]
+        output = ndimage.rank_filter(array, -2, size=3)
+        assert_array_almost_equal(expected, output)
+
+    def test_rank06(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]])
+        expected = [[2, 2, 1, 1, 1],
+                    [3, 3, 2, 1, 1],
+                    [5, 5, 3, 3, 1]]
+        output = ndimage.rank_filter(array, 1, size=[2, 3])
+        assert_array_almost_equal(expected, output)
+        output = ndimage.percentile_filter(array, 17, size=(2, 3))
+        assert_array_almost_equal(expected, output)
+
+    def test_rank06_overlap(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]])
+        array_copy = array.copy()
+        expected = [[2, 2, 1, 1, 1],
+                    [3, 3, 2, 1, 1],
+                    [5, 5, 3, 3, 1]]
+        ndimage.rank_filter(array, 1, size=[2, 3], output=array)
+        assert_array_almost_equal(expected, array)
+
+        ndimage.percentile_filter(array_copy, 17, size=(2, 3),
+                                  output=array_copy)
+        assert_array_almost_equal(expected, array_copy)
+
+    def test_rank07(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]])
+        expected = [[3, 5, 5, 5, 4],
+                    [5, 5, 7, 5, 4],
+                    [6, 8, 8, 7, 5]]
+        output = ndimage.rank_filter(array, -2, size=[2, 3])
+        assert_array_almost_equal(expected, output)
+
+    def test_rank08(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]])
+        expected = [[3, 3, 2, 4, 4],
+                    [5, 5, 5, 4, 4],
+                    [5, 6, 7, 5, 5]]
+        output = ndimage.percentile_filter(array, 50.0, size=(2, 3))
+        assert_array_almost_equal(expected, output)
+        output = ndimage.rank_filter(array, 3, size=(2, 3))
+        assert_array_almost_equal(expected, output)
+        output = ndimage.median_filter(array, size=(2, 3))
+        assert_array_almost_equal(expected, output)
+
+        # non-separable: does not allow mode sequence
+        with assert_raises(RuntimeError):
+            ndimage.percentile_filter(array, 50.0, size=(2, 3),
+                                      mode=['reflect', 'constant'])
+        with assert_raises(RuntimeError):
+            ndimage.rank_filter(array, 3, size=(2, 3), mode=['reflect']*2)
+        with assert_raises(RuntimeError):
+            ndimage.median_filter(array, size=(2, 3), mode=['reflect']*2)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_rank09(self, dtype):
+        expected = [[3, 3, 2, 4, 4],
+                    [3, 5, 2, 5, 1],
+                    [5, 5, 8, 3, 5]]
+        footprint = [[1, 0, 1], [0, 1, 0]]
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        output = ndimage.rank_filter(array, 1, footprint=footprint)
+        assert_array_almost_equal(expected, output)
+        output = ndimage.percentile_filter(array, 35, footprint=footprint)
+        assert_array_almost_equal(expected, output)
+
+    def test_rank10(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        expected = [[2, 2, 1, 1, 1],
+                    [2, 3, 1, 3, 1],
+                    [5, 5, 3, 3, 1]]
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        output = ndimage.rank_filter(array, 0, footprint=footprint)
+        assert_array_almost_equal(expected, output)
+        output = ndimage.percentile_filter(array, 0.0, footprint=footprint)
+        assert_array_almost_equal(expected, output)
+
+    def test_rank11(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        expected = [[3, 5, 5, 5, 4],
+                    [7, 7, 9, 9, 5],
+                    [7, 9, 8, 9, 7]]
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        output = ndimage.rank_filter(array, -1, footprint=footprint)
+        assert_array_almost_equal(expected, output)
+        output = ndimage.percentile_filter(array, 100.0, footprint=footprint)
+        assert_array_almost_equal(expected, output)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_rank12(self, dtype):
+        expected = [[3, 3, 2, 4, 4],
+                    [3, 5, 2, 5, 1],
+                    [5, 5, 8, 3, 5]]
+        footprint = [[1, 0, 1], [0, 1, 0]]
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        output = ndimage.rank_filter(array, 1, footprint=footprint)
+        assert_array_almost_equal(expected, output)
+        output = ndimage.percentile_filter(array, 50.0,
+                                           footprint=footprint)
+        assert_array_almost_equal(expected, output)
+        output = ndimage.median_filter(array, footprint=footprint)
+        assert_array_almost_equal(expected, output)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_rank13(self, dtype):
+        expected = [[5, 2, 5, 1, 1],
+                    [5, 8, 3, 5, 5],
+                    [6, 6, 5, 5, 5]]
+        footprint = [[1, 0, 1], [0, 1, 0]]
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        output = ndimage.rank_filter(array, 1, footprint=footprint,
+                                     origin=-1)
+        assert_array_almost_equal(expected, output)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_rank14(self, dtype):
+        expected = [[3, 5, 2, 5, 1],
+                    [5, 5, 8, 3, 5],
+                    [5, 6, 6, 5, 5]]
+        footprint = [[1, 0, 1], [0, 1, 0]]
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        output = ndimage.rank_filter(array, 1, footprint=footprint,
+                                     origin=[-1, 0])
+        assert_array_almost_equal(expected, output)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_rank15(self, dtype):
+        expected = [[2, 3, 1, 4, 1],
+                    [5, 3, 7, 1, 1],
+                    [5, 5, 3, 3, 3]]
+        footprint = [[1, 0, 1], [0, 1, 0]]
+        array = np.array([[3, 2, 5, 1, 4],
+                          [5, 8, 3, 7, 1],
+                          [5, 6, 9, 3, 5]], dtype)
+        output = ndimage.rank_filter(array, 0, footprint=footprint,
+                                     origin=[-1, 0])
+        assert_array_almost_equal(expected, output)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_generic_filter1d01(self, dtype):
+        weights = np.array([1.1, 2.2, 3.3])
+
+        def _filter_func(input, output, fltr, total):
+            fltr = fltr / total
+            for ii in range(input.shape[0] - 2):
+                output[ii] = input[ii] * fltr[0]
+                output[ii] += input[ii + 1] * fltr[1]
+                output[ii] += input[ii + 2] * fltr[2]
+        a = np.arange(12, dtype=dtype)
+        a.shape = (3, 4)
+        r1 = ndimage.correlate1d(a, weights / weights.sum(), 0, origin=-1)
+        r2 = ndimage.generic_filter1d(
+            a, _filter_func, 3, axis=0, origin=-1,
+            extra_arguments=(weights,),
+            extra_keywords={'total': weights.sum()})
+        assert_array_almost_equal(r1, r2)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_generic_filter01(self, dtype):
+        filter_ = np.array([[1.0, 2.0], [3.0, 4.0]])
+        footprint = np.array([[1, 0], [0, 1]])
+        cf = np.array([1., 4.])
+
+        def _filter_func(buffer, weights, total=1.0):
+            weights = cf / total
+            return (buffer * weights).sum()
+
+        a = np.arange(12, dtype=dtype)
+        a.shape = (3, 4)
+        r1 = ndimage.correlate(a, filter_ * footprint)
+        if dtype in float_types:
+            r1 /= 5
+        else:
+            r1 //= 5
+        r2 = ndimage.generic_filter(
+            a, _filter_func, footprint=footprint, extra_arguments=(cf,),
+            extra_keywords={'total': cf.sum()})
+        assert_array_almost_equal(r1, r2)
+
+        # generic_filter doesn't allow mode sequence
+        with assert_raises(RuntimeError):
+            r2 = ndimage.generic_filter(
+                a, _filter_func, mode=['reflect', 'reflect'],
+                footprint=footprint, extra_arguments=(cf,),
+                extra_keywords={'total': cf.sum()})
+
+    @pytest.mark.parametrize(
+        'mode, expected_value',
+        [('nearest', [1, 1, 2]),
+         ('wrap', [3, 1, 2]),
+         ('reflect', [1, 1, 2]),
+         ('mirror', [2, 1, 2]),
+         ('constant', [0, 1, 2])]
+    )
+    def test_extend01(self, mode, expected_value):
+        array = np.array([1, 2, 3])
+        weights = np.array([1, 0])
+        output = ndimage.correlate1d(array, weights, 0, mode=mode, cval=0)
+        assert_array_equal(output, expected_value)
+
+    @pytest.mark.parametrize(
+        'mode, expected_value',
+        [('nearest', [1, 1, 1]),
+         ('wrap', [3, 1, 2]),
+         ('reflect', [3, 3, 2]),
+         ('mirror', [1, 2, 3]),
+         ('constant', [0, 0, 0])]
+    )
+    def test_extend02(self, mode, expected_value):
+        array = np.array([1, 2, 3])
+        weights = np.array([1, 0, 0, 0, 0, 0, 0, 0])
+        output = ndimage.correlate1d(array, weights, 0, mode=mode, cval=0)
+        assert_array_equal(output, expected_value)
+
+    @pytest.mark.parametrize(
+        'mode, expected_value',
+        [('nearest', [2, 3, 3]),
+         ('wrap', [2, 3, 1]),
+         ('reflect', [2, 3, 3]),
+         ('mirror', [2, 3, 2]),
+         ('constant', [2, 3, 0])]
+    )
+    def test_extend03(self, mode, expected_value):
+        array = np.array([1, 2, 3])
+        weights = np.array([0, 0, 1])
+        output = ndimage.correlate1d(array, weights, 0, mode=mode, cval=0)
+        assert_array_equal(output, expected_value)
+
+    @pytest.mark.parametrize(
+        'mode, expected_value',
+        [('nearest', [3, 3, 3]),
+         ('wrap', [2, 3, 1]),
+         ('reflect', [2, 1, 1]),
+         ('mirror', [1, 2, 3]),
+         ('constant', [0, 0, 0])]
+    )
+    def test_extend04(self, mode, expected_value):
+        array = np.array([1, 2, 3])
+        weights = np.array([0, 0, 0, 0, 0, 0, 0, 0, 1])
+        output = ndimage.correlate1d(array, weights, 0, mode=mode, cval=0)
+        assert_array_equal(output, expected_value)
+
+    @pytest.mark.parametrize(
+        'mode, expected_value',
+        [('nearest', [[1, 1, 2], [1, 1, 2], [4, 4, 5]]),
+         ('wrap', [[9, 7, 8], [3, 1, 2], [6, 4, 5]]),
+         ('reflect', [[1, 1, 2], [1, 1, 2], [4, 4, 5]]),
+         ('mirror', [[5, 4, 5], [2, 1, 2], [5, 4, 5]]),
+         ('constant', [[0, 0, 0], [0, 1, 2], [0, 4, 5]])]
+    )
+    def test_extend05(self, mode, expected_value):
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6],
+                          [7, 8, 9]])
+        weights = np.array([[1, 0], [0, 0]])
+        output = ndimage.correlate(array, weights, mode=mode, cval=0)
+        assert_array_equal(output, expected_value)
+
+    @pytest.mark.parametrize(
+        'mode, expected_value',
+        [('nearest', [[5, 6, 6], [8, 9, 9], [8, 9, 9]]),
+         ('wrap', [[5, 6, 4], [8, 9, 7], [2, 3, 1]]),
+         ('reflect', [[5, 6, 6], [8, 9, 9], [8, 9, 9]]),
+         ('mirror', [[5, 6, 5], [8, 9, 8], [5, 6, 5]]),
+         ('constant', [[5, 6, 0], [8, 9, 0], [0, 0, 0]])]
+    )
+    def test_extend06(self, mode, expected_value):
+        array = np.array([[1, 2, 3],
+                          [4, 5, 6],
+                          [7, 8, 9]])
+        weights = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 1]])
+        output = ndimage.correlate(array, weights, mode=mode, cval=0)
+        assert_array_equal(output, expected_value)
+
+    @pytest.mark.parametrize(
+        'mode, expected_value',
+        [('nearest', [3, 3, 3]),
+         ('wrap', [2, 3, 1]),
+         ('reflect', [2, 1, 1]),
+         ('mirror', [1, 2, 3]),
+         ('constant', [0, 0, 0])]
+    )
+    def test_extend07(self, mode, expected_value):
+        array = np.array([1, 2, 3])
+        weights = np.array([0, 0, 0, 0, 0, 0, 0, 0, 1])
+        output = ndimage.correlate(array, weights, mode=mode, cval=0)
+        assert_array_equal(output, expected_value)
+
+    @pytest.mark.parametrize(
+        'mode, expected_value',
+        [('nearest', [[3], [3], [3]]),
+         ('wrap', [[2], [3], [1]]),
+         ('reflect', [[2], [1], [1]]),
+         ('mirror', [[1], [2], [3]]),
+         ('constant', [[0], [0], [0]])]
+    )
+    def test_extend08(self, mode, expected_value):
+        array = np.array([[1], [2], [3]])
+        weights = np.array([[0], [0], [0], [0], [0], [0], [0], [0], [1]])
+        output = ndimage.correlate(array, weights, mode=mode, cval=0)
+        assert_array_equal(output, expected_value)
+
+    @pytest.mark.parametrize(
+        'mode, expected_value',
+        [('nearest', [3, 3, 3]),
+         ('wrap', [2, 3, 1]),
+         ('reflect', [2, 1, 1]),
+         ('mirror', [1, 2, 3]),
+         ('constant', [0, 0, 0])]
+    )
+    def test_extend09(self, mode, expected_value):
+        array = np.array([1, 2, 3])
+        weights = np.array([0, 0, 0, 0, 0, 0, 0, 0, 1])
+        output = ndimage.correlate(array, weights, mode=mode, cval=0)
+        assert_array_equal(output, expected_value)
+
+    @pytest.mark.parametrize(
+        'mode, expected_value',
+        [('nearest', [[3], [3], [3]]),
+         ('wrap', [[2], [3], [1]]),
+         ('reflect', [[2], [1], [1]]),
+         ('mirror', [[1], [2], [3]]),
+         ('constant', [[0], [0], [0]])]
+    )
+    def test_extend10(self, mode, expected_value):
+        array = np.array([[1], [2], [3]])
+        weights = np.array([[0], [0], [0], [0], [0], [0], [0], [0], [1]])
+        output = ndimage.correlate(array, weights, mode=mode, cval=0)
+        assert_array_equal(output, expected_value)
+
+
+def test_ticket_701():
+    # Test generic filter sizes
+    arr = np.arange(4).reshape((2, 2))
+    def func(x):
+        return np.min(x)
+    res = ndimage.generic_filter(arr, func, size=(1, 1))
+    # The following raises an error unless ticket 701 is fixed
+    res2 = ndimage.generic_filter(arr, func, size=1)
+    assert_equal(res, res2)
+
+
+def test_gh_5430():
+    # At least one of these raises an error unless gh-5430 is
+    # fixed. In py2k an int is implemented using a C long, so
+    # which one fails depends on your system. In py3k there is only
+    # one arbitrary precision integer type, so both should fail.
+    sigma = np.int32(1)
+    out = ndimage._ni_support._normalize_sequence(sigma, 1)
+    assert_equal(out, [sigma])
+    sigma = np.int64(1)
+    out = ndimage._ni_support._normalize_sequence(sigma, 1)
+    assert_equal(out, [sigma])
+    # This worked before; make sure it still works
+    sigma = 1
+    out = ndimage._ni_support._normalize_sequence(sigma, 1)
+    assert_equal(out, [sigma])
+    # This worked before; make sure it still works
+    sigma = [1, 1]
+    out = ndimage._ni_support._normalize_sequence(sigma, 2)
+    assert_equal(out, sigma)
+    # Also include the OPs original example to make sure we fixed the issue
+    x = np.random.normal(size=(256, 256))
+    perlin = np.zeros_like(x)
+    for i in 2**np.arange(6):
+        perlin += ndimage.gaussian_filter(x, i, mode="wrap") * i**2
+    # This also fixes gh-4106, show that the OPs example now runs.
+    x = np.int64(21)
+    ndimage._ni_support._normalize_sequence(x, 0)
+
+
+def test_gaussian_kernel1d():
+    radius = 10
+    sigma = 2
+    sigma2 = sigma * sigma
+    x = np.arange(-radius, radius + 1, dtype=np.double)
+    phi_x = np.exp(-0.5 * x * x / sigma2)
+    phi_x /= phi_x.sum()
+    assert_allclose(phi_x, _gaussian_kernel1d(sigma, 0, radius))
+    assert_allclose(-phi_x * x / sigma2, _gaussian_kernel1d(sigma, 1, radius))
+    assert_allclose(phi_x * (x * x / sigma2 - 1) / sigma2,
+                    _gaussian_kernel1d(sigma, 2, radius))
+    assert_allclose(phi_x * (3 - x * x / sigma2) * x / (sigma2 * sigma2),
+                    _gaussian_kernel1d(sigma, 3, radius))
+
+
+def test_orders_gauss():
+    # Check order inputs to Gaussians
+    arr = np.zeros((1,))
+    assert_equal(0, ndimage.gaussian_filter(arr, 1, order=0))
+    assert_equal(0, ndimage.gaussian_filter(arr, 1, order=3))
+    assert_raises(ValueError, ndimage.gaussian_filter, arr, 1, -1)
+    assert_equal(0, ndimage.gaussian_filter1d(arr, 1, axis=-1, order=0))
+    assert_equal(0, ndimage.gaussian_filter1d(arr, 1, axis=-1, order=3))
+    assert_raises(ValueError, ndimage.gaussian_filter1d, arr, 1, -1, -1)
+
+
+def test_valid_origins():
+    """Regression test for #1311."""
+    def func(x):
+        return np.mean(x)
+    data = np.array([1, 2, 3, 4, 5], dtype=np.float64)
+    assert_raises(ValueError, ndimage.generic_filter, data, func, size=3,
+                  origin=2)
+    assert_raises(ValueError, ndimage.generic_filter1d, data, func,
+                  filter_size=3, origin=2)
+    assert_raises(ValueError, ndimage.percentile_filter, data, 0.2, size=3,
+                  origin=2)
+
+    for filter in [ndimage.uniform_filter, ndimage.minimum_filter,
+                   ndimage.maximum_filter, ndimage.maximum_filter1d,
+                   ndimage.median_filter, ndimage.minimum_filter1d]:
+        # This should work, since for size == 3, the valid range for origin is
+        # -1 to 1.
+        list(filter(data, 3, origin=-1))
+        list(filter(data, 3, origin=1))
+        # Just check this raises an error instead of silently accepting or
+        # segfaulting.
+        assert_raises(ValueError, filter, data, 3, origin=2)
+
+
+def test_bad_convolve_and_correlate_origins():
+    """Regression test for gh-822."""
+    # Before gh-822 was fixed, these would generate seg. faults or
+    # other crashes on many system.
+    assert_raises(ValueError, ndimage.correlate1d,
+                  [0, 1, 2, 3, 4, 5], [1, 1, 2, 0], origin=2)
+    assert_raises(ValueError, ndimage.correlate,
+                  [0, 1, 2, 3, 4, 5], [0, 1, 2], origin=[2])
+    assert_raises(ValueError, ndimage.correlate,
+                  np.ones((3, 5)), np.ones((2, 2)), origin=[0, 1])
+
+    assert_raises(ValueError, ndimage.convolve1d,
+                  np.arange(10), np.ones(3), origin=-2)
+    assert_raises(ValueError, ndimage.convolve,
+                  np.arange(10), np.ones(3), origin=[-2])
+    assert_raises(ValueError, ndimage.convolve,
+                  np.ones((3, 5)), np.ones((2, 2)), origin=[0, -2])
+
+
+def test_multiple_modes():
+    # Test that the filters with multiple mode cababilities for different
+    # dimensions give the same result as applying a single mode.
+    arr = np.array([[1., 0., 0.],
+                       [1., 1., 0.],
+                       [0., 0., 0.]])
+
+    mode1 = 'reflect'
+    mode2 = ['reflect', 'reflect']
+
+    assert_equal(ndimage.gaussian_filter(arr, 1, mode=mode1),
+                 ndimage.gaussian_filter(arr, 1, mode=mode2))
+    assert_equal(ndimage.prewitt(arr, mode=mode1),
+                 ndimage.prewitt(arr, mode=mode2))
+    assert_equal(ndimage.sobel(arr, mode=mode1),
+                 ndimage.sobel(arr, mode=mode2))
+    assert_equal(ndimage.laplace(arr, mode=mode1),
+                 ndimage.laplace(arr, mode=mode2))
+    assert_equal(ndimage.gaussian_laplace(arr, 1, mode=mode1),
+                 ndimage.gaussian_laplace(arr, 1, mode=mode2))
+    assert_equal(ndimage.maximum_filter(arr, size=5, mode=mode1),
+                 ndimage.maximum_filter(arr, size=5, mode=mode2))
+    assert_equal(ndimage.minimum_filter(arr, size=5, mode=mode1),
+                 ndimage.minimum_filter(arr, size=5, mode=mode2))
+    assert_equal(ndimage.gaussian_gradient_magnitude(arr, 1, mode=mode1),
+                 ndimage.gaussian_gradient_magnitude(arr, 1, mode=mode2))
+    assert_equal(ndimage.uniform_filter(arr, 5, mode=mode1),
+                 ndimage.uniform_filter(arr, 5, mode=mode2))
+
+
+def test_multiple_modes_sequentially():
+    # Test that the filters with multiple mode cababilities for different
+    # dimensions give the same result as applying the filters with
+    # different modes sequentially
+    arr = np.array([[1., 0., 0.],
+                    [1., 1., 0.],
+                    [0., 0., 0.]])
+
+    modes = ['reflect', 'wrap']
+
+    expected = ndimage.gaussian_filter1d(arr, 1, axis=0, mode=modes[0])
+    expected = ndimage.gaussian_filter1d(expected, 1, axis=1, mode=modes[1])
+    assert_equal(expected,
+                 ndimage.gaussian_filter(arr, 1, mode=modes))
+
+    expected = ndimage.uniform_filter1d(arr, 5, axis=0, mode=modes[0])
+    expected = ndimage.uniform_filter1d(expected, 5, axis=1, mode=modes[1])
+    assert_equal(expected,
+                 ndimage.uniform_filter(arr, 5, mode=modes))
+
+    expected = ndimage.maximum_filter1d(arr, size=5, axis=0, mode=modes[0])
+    expected = ndimage.maximum_filter1d(expected, size=5, axis=1,
+                                        mode=modes[1])
+    assert_equal(expected,
+                 ndimage.maximum_filter(arr, size=5, mode=modes))
+
+    expected = ndimage.minimum_filter1d(arr, size=5, axis=0, mode=modes[0])
+    expected = ndimage.minimum_filter1d(expected, size=5, axis=1,
+                                        mode=modes[1])
+    assert_equal(expected,
+                 ndimage.minimum_filter(arr, size=5, mode=modes))
+
+
+def test_multiple_modes_prewitt():
+    # Test prewitt filter for multiple extrapolation modes
+    arr = np.array([[1., 0., 0.],
+                    [1., 1., 0.],
+                    [0., 0., 0.]])
+
+    expected = np.array([[1., -3., 2.],
+                         [1., -2., 1.],
+                         [1., -1., 0.]])
+
+    modes = ['reflect', 'wrap']
+
+    assert_equal(expected,
+                 ndimage.prewitt(arr, mode=modes))
+
+
+def test_multiple_modes_sobel():
+    # Test sobel filter for multiple extrapolation modes
+    arr = np.array([[1., 0., 0.],
+                    [1., 1., 0.],
+                    [0., 0., 0.]])
+
+    expected = np.array([[1., -4., 3.],
+                         [2., -3., 1.],
+                         [1., -1., 0.]])
+
+    modes = ['reflect', 'wrap']
+
+    assert_equal(expected,
+                 ndimage.sobel(arr, mode=modes))
+
+
+def test_multiple_modes_laplace():
+    # Test laplace filter for multiple extrapolation modes
+    arr = np.array([[1., 0., 0.],
+                    [1., 1., 0.],
+                    [0., 0., 0.]])
+
+    expected = np.array([[-2., 2., 1.],
+                         [-2., -3., 2.],
+                         [1., 1., 0.]])
+
+    modes = ['reflect', 'wrap']
+
+    assert_equal(expected,
+                 ndimage.laplace(arr, mode=modes))
+
+
+def test_multiple_modes_gaussian_laplace():
+    # Test gaussian_laplace filter for multiple extrapolation modes
+    arr = np.array([[1., 0., 0.],
+                    [1., 1., 0.],
+                    [0., 0., 0.]])
+
+    expected = np.array([[-0.28438687, 0.01559809, 0.19773499],
+                         [-0.36630503, -0.20069774, 0.07483620],
+                         [0.15849176, 0.18495566, 0.21934094]])
+
+    modes = ['reflect', 'wrap']
+
+    assert_almost_equal(expected,
+                        ndimage.gaussian_laplace(arr, 1, mode=modes))
+
+
+def test_multiple_modes_gaussian_gradient_magnitude():
+    # Test gaussian_gradient_magnitude filter for multiple
+    # extrapolation modes
+    arr = np.array([[1., 0., 0.],
+                    [1., 1., 0.],
+                    [0., 0., 0.]])
+
+    expected = np.array([[0.04928965, 0.09745625, 0.06405368],
+                         [0.23056905, 0.14025305, 0.04550846],
+                         [0.19894369, 0.14950060, 0.06796850]])
+
+    modes = ['reflect', 'wrap']
+
+    calculated = ndimage.gaussian_gradient_magnitude(arr, 1, mode=modes)
+
+    assert_almost_equal(expected, calculated)
+
+
+def test_multiple_modes_uniform():
+    # Test uniform filter for multiple extrapolation modes
+    arr = np.array([[1., 0., 0.],
+                    [1., 1., 0.],
+                    [0., 0., 0.]])
+
+    expected = np.array([[0.32, 0.40, 0.48],
+                         [0.20, 0.28, 0.32],
+                         [0.28, 0.32, 0.40]])
+
+    modes = ['reflect', 'wrap']
+
+    assert_almost_equal(expected,
+                        ndimage.uniform_filter(arr, 5, mode=modes))
+
+
+def test_gaussian_truncate():
+    # Test that Gaussian filters can be truncated at different widths.
+    # These tests only check that the result has the expected number
+    # of nonzero elements.
+    arr = np.zeros((100, 100), float)
+    arr[50, 50] = 1
+    num_nonzeros_2 = (ndimage.gaussian_filter(arr, 5, truncate=2) > 0).sum()
+    assert_equal(num_nonzeros_2, 21**2)
+    num_nonzeros_5 = (ndimage.gaussian_filter(arr, 5, truncate=5) > 0).sum()
+    assert_equal(num_nonzeros_5, 51**2)
+
+    # Test truncate when sigma is a sequence.
+    f = ndimage.gaussian_filter(arr, [0.5, 2.5], truncate=3.5)
+    fpos = f > 0
+    n0 = fpos.any(axis=0).sum()
+    # n0 should be 2*int(2.5*3.5 + 0.5) + 1
+    assert_equal(n0, 19)
+    n1 = fpos.any(axis=1).sum()
+    # n1 should be 2*int(0.5*3.5 + 0.5) + 1
+    assert_equal(n1, 5)
+
+    # Test gaussian_filter1d.
+    x = np.zeros(51)
+    x[25] = 1
+    f = ndimage.gaussian_filter1d(x, sigma=2, truncate=3.5)
+    n = (f > 0).sum()
+    assert_equal(n, 15)
+
+    # Test gaussian_laplace
+    y = ndimage.gaussian_laplace(x, sigma=2, truncate=3.5)
+    nonzero_indices = np.nonzero(y != 0)[0]
+    n = np.ptp(nonzero_indices) + 1
+    assert_equal(n, 15)
+
+    # Test gaussian_gradient_magnitude
+    y = ndimage.gaussian_gradient_magnitude(x, sigma=2, truncate=3.5)
+    nonzero_indices = np.nonzero(y != 0)[0]
+    n = np.ptp(nonzero_indices) + 1
+    assert_equal(n, 15)
+
+
+def test_gaussian_radius():
+    # Test that Gaussian filters with radius argument produce the same
+    # results as the filters with corresponding truncate argument.
+    # radius = int(truncate * sigma + 0.5)
+    # Test gaussian_filter1d
+    x = np.zeros(7)
+    x[3] = 1
+    f1 = ndimage.gaussian_filter1d(x, sigma=2, truncate=1.5)
+    f2 = ndimage.gaussian_filter1d(x, sigma=2, radius=3)
+    assert_equal(f1, f2)
+
+    # Test gaussian_filter when sigma is a number.
+    a = np.zeros((9, 9))
+    a[4, 4] = 1
+    f1 = ndimage.gaussian_filter(a, sigma=0.5, truncate=3.5)
+    f2 = ndimage.gaussian_filter(a, sigma=0.5, radius=2)
+    assert_equal(f1, f2)
+
+    # Test gaussian_filter when sigma is a sequence.
+    a = np.zeros((50, 50))
+    a[25, 25] = 1
+    f1 = ndimage.gaussian_filter(a, sigma=[0.5, 2.5], truncate=3.5)
+    f2 = ndimage.gaussian_filter(a, sigma=[0.5, 2.5], radius=[2, 9])
+    assert_equal(f1, f2)
+
+
+def test_gaussian_radius_invalid():
+    # radius must be a nonnegative integer
+    with assert_raises(ValueError):
+        ndimage.gaussian_filter1d(np.zeros(8), sigma=1, radius=-1)
+    with assert_raises(ValueError):
+        ndimage.gaussian_filter1d(np.zeros(8), sigma=1, radius=1.1)
+
+
+class TestThreading:
+    def check_func_thread(self, n, fun, args, out):
+        from threading import Thread
+        thrds = [Thread(target=fun, args=args, kwargs={'output': out[x]})
+                 for x in range(n)]
+        [t.start() for t in thrds]
+        [t.join() for t in thrds]
+
+    def check_func_serial(self, n, fun, args, out):
+        for i in range(n):
+            fun(*args, output=out[i])
+
+    def test_correlate1d(self):
+        d = np.random.randn(5000)
+        os = np.empty((4, d.size))
+        ot = np.empty_like(os)
+        k = np.arange(5)
+        self.check_func_serial(4, ndimage.correlate1d, (d, k), os)
+        self.check_func_thread(4, ndimage.correlate1d, (d, k), ot)
+        assert_array_equal(os, ot)
+
+    def test_correlate(self):
+        d = np.random.randn(500, 500)
+        k = np.random.randn(10, 10)
+        os = np.empty([4] + list(d.shape))
+        ot = np.empty_like(os)
+        self.check_func_serial(4, ndimage.correlate, (d, k), os)
+        self.check_func_thread(4, ndimage.correlate, (d, k), ot)
+        assert_array_equal(os, ot)
+
+    def test_median_filter(self):
+        d = np.random.randn(500, 500)
+        os = np.empty([4] + list(d.shape))
+        ot = np.empty_like(os)
+        self.check_func_serial(4, ndimage.median_filter, (d, 3), os)
+        self.check_func_thread(4, ndimage.median_filter, (d, 3), ot)
+        assert_array_equal(os, ot)
+
+    def test_uniform_filter1d(self):
+        d = np.random.randn(5000)
+        os = np.empty((4, d.size))
+        ot = np.empty_like(os)
+        self.check_func_serial(4, ndimage.uniform_filter1d, (d, 5), os)
+        self.check_func_thread(4, ndimage.uniform_filter1d, (d, 5), ot)
+        assert_array_equal(os, ot)
+
+    def test_minmax_filter(self):
+        d = np.random.randn(500, 500)
+        os = np.empty([4] + list(d.shape))
+        ot = np.empty_like(os)
+        self.check_func_serial(4, ndimage.maximum_filter, (d, 3), os)
+        self.check_func_thread(4, ndimage.maximum_filter, (d, 3), ot)
+        assert_array_equal(os, ot)
+        self.check_func_serial(4, ndimage.minimum_filter, (d, 3), os)
+        self.check_func_thread(4, ndimage.minimum_filter, (d, 3), ot)
+        assert_array_equal(os, ot)
+
+
+def test_minmaximum_filter1d():
+    # Regression gh-3898
+    in_ = np.arange(10)
+    out = ndimage.minimum_filter1d(in_, 1)
+    assert_equal(in_, out)
+    out = ndimage.maximum_filter1d(in_, 1)
+    assert_equal(in_, out)
+    # Test reflect
+    out = ndimage.minimum_filter1d(in_, 5, mode='reflect')
+    assert_equal([0, 0, 0, 1, 2, 3, 4, 5, 6, 7], out)
+    out = ndimage.maximum_filter1d(in_, 5, mode='reflect')
+    assert_equal([2, 3, 4, 5, 6, 7, 8, 9, 9, 9], out)
+    # Test constant
+    out = ndimage.minimum_filter1d(in_, 5, mode='constant', cval=-1)
+    assert_equal([-1, -1, 0, 1, 2, 3, 4, 5, -1, -1], out)
+    out = ndimage.maximum_filter1d(in_, 5, mode='constant', cval=10)
+    assert_equal([10, 10, 4, 5, 6, 7, 8, 9, 10, 10], out)
+    # Test nearest
+    out = ndimage.minimum_filter1d(in_, 5, mode='nearest')
+    assert_equal([0, 0, 0, 1, 2, 3, 4, 5, 6, 7], out)
+    out = ndimage.maximum_filter1d(in_, 5, mode='nearest')
+    assert_equal([2, 3, 4, 5, 6, 7, 8, 9, 9, 9], out)
+    # Test wrap
+    out = ndimage.minimum_filter1d(in_, 5, mode='wrap')
+    assert_equal([0, 0, 0, 1, 2, 3, 4, 5, 0, 0], out)
+    out = ndimage.maximum_filter1d(in_, 5, mode='wrap')
+    assert_equal([9, 9, 4, 5, 6, 7, 8, 9, 9, 9], out)
+
+
+def test_uniform_filter1d_roundoff_errors():
+    # gh-6930
+    in_ = np.repeat([0, 1, 0], [9, 9, 9])
+    for filter_size in range(3, 10):
+        out = ndimage.uniform_filter1d(in_, filter_size)
+        assert_equal(out.sum(), 10 - filter_size)
+
+
+def test_footprint_all_zeros():
+    # regression test for gh-6876: footprint of all zeros segfaults
+    arr = np.random.randint(0, 100, (100, 100))
+    kernel = np.zeros((3, 3), bool)
+    with assert_raises(ValueError):
+        ndimage.maximum_filter(arr, footprint=kernel)
+
+
+def test_gaussian_filter():
+    # Test gaussian filter with np.float16
+    # gh-8207
+    data = np.array([1], dtype=np.float16)
+    sigma = 1.0
+    with assert_raises(RuntimeError):
+        ndimage.gaussian_filter(data, sigma)
+
+
+def test_rank_filter_noninteger_rank():
+    # regression test for issue 9388: ValueError for
+    # non integer rank when performing rank_filter
+    arr = np.random.random((10, 20, 30))
+    assert_raises(TypeError, ndimage.rank_filter, arr, 0.5,
+                  footprint=np.ones((1, 1, 10), dtype=bool))
+
+
+def test_size_footprint_both_set():
+    # test for input validation, expect user warning when
+    # size and footprint is set
+    with suppress_warnings() as sup:
+        sup.filter(UserWarning,
+                   "ignoring size because footprint is set")
+        arr = np.random.random((10, 20, 30))
+        ndimage.rank_filter(arr, 5, size=2, footprint=np.ones((1, 1, 10), dtype=bool))
+
+
+def test_byte_order_median():
+    """Regression test for #413: median_filter does not handle bytes orders."""
+    a = np.arange(9, dtype=' 3 raise NotImplementedError
+        x = np.ones((4, 6, 8, 10), dtype=np.complex128)
+        with pytest.raises(NotImplementedError):
+            ndimage.fourier_ellipsoid(x, 3)
+
+    def test_fourier_ellipsoid_1d_complex(self):
+        # expected result of 1d ellipsoid is the same as for fourier_uniform
+        for shape in [(32, ), (31, )]:
+            for type_, dec in zip([np.complex64, np.complex128], [5, 14]):
+                x = np.ones(shape, dtype=type_)
+                a = ndimage.fourier_ellipsoid(x, 5, -1, 0)
+                b = ndimage.fourier_uniform(x, 5, -1, 0)
+                assert_array_almost_equal(a, b, decimal=dec)
+
+    @pytest.mark.parametrize('shape', [(0, ), (0, 10), (10, 0)])
+    @pytest.mark.parametrize('dtype', [np.float32, np.float64,
+                                       np.complex64, np.complex128])
+    @pytest.mark.parametrize('test_func',
+                             [ndimage.fourier_ellipsoid,
+                              ndimage.fourier_gaussian,
+                              ndimage.fourier_uniform])
+    def test_fourier_zero_length_dims(self, shape, dtype, test_func):
+        a = np.ones(shape, dtype)
+        b = test_func(a, 3)
+        assert_equal(a, b)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_interpolation.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_interpolation.py
new file mode 100644
index 0000000000000000000000000000000000000000..c92cfb558a0fafac3b881540afaf6b05165f5dc5
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_interpolation.py
@@ -0,0 +1,1327 @@
+import sys
+
+import numpy as np
+from numpy.testing import (assert_, assert_equal, assert_array_equal,
+                           assert_array_almost_equal, assert_allclose,
+                           suppress_warnings)
+import pytest
+from pytest import raises as assert_raises
+import scipy.ndimage as ndimage
+
+from . import types
+
+eps = 1e-12
+
+ndimage_to_numpy_mode = {
+    'mirror': 'reflect',
+    'reflect': 'symmetric',
+    'grid-mirror': 'symmetric',
+    'grid-wrap': 'wrap',
+    'nearest': 'edge',
+    'grid-constant': 'constant',
+}
+
+
+class TestNdimageInterpolation:
+
+    @pytest.mark.parametrize(
+        'mode, expected_value',
+        [('nearest', [1.5, 2.5, 3.5, 4, 4, 4, 4]),
+         ('wrap', [1.5, 2.5, 3.5, 1.5, 2.5, 3.5, 1.5]),
+         ('grid-wrap', [1.5, 2.5, 3.5, 2.5, 1.5, 2.5, 3.5]),
+         ('mirror', [1.5, 2.5, 3.5, 3.5, 2.5, 1.5, 1.5]),
+         ('reflect', [1.5, 2.5, 3.5, 4, 3.5, 2.5, 1.5]),
+         ('constant', [1.5, 2.5, 3.5, -1, -1, -1, -1]),
+         ('grid-constant', [1.5, 2.5, 3.5, 1.5, -1, -1, -1])]
+    )
+    def test_boundaries(self, mode, expected_value):
+        def shift(x):
+            return (x[0] + 0.5,)
+
+        data = np.array([1, 2, 3, 4.])
+        assert_array_equal(
+            expected_value,
+            ndimage.geometric_transform(data, shift, cval=-1, mode=mode,
+                                        output_shape=(7,), order=1))
+
+    @pytest.mark.parametrize(
+        'mode, expected_value',
+        [('nearest', [1, 1, 2, 3]),
+         ('wrap', [3, 1, 2, 3]),
+         ('grid-wrap', [4, 1, 2, 3]),
+         ('mirror', [2, 1, 2, 3]),
+         ('reflect', [1, 1, 2, 3]),
+         ('constant', [-1, 1, 2, 3]),
+         ('grid-constant', [-1, 1, 2, 3])]
+    )
+    def test_boundaries2(self, mode, expected_value):
+        def shift(x):
+            return (x[0] - 0.9,)
+
+        data = np.array([1, 2, 3, 4])
+        assert_array_equal(
+            expected_value,
+            ndimage.geometric_transform(data, shift, cval=-1, mode=mode,
+                                        output_shape=(4,)))
+
+    @pytest.mark.parametrize('mode', ['mirror', 'reflect', 'grid-mirror',
+                                      'grid-wrap', 'grid-constant',
+                                      'nearest'])
+    @pytest.mark.parametrize('order', range(6))
+    def test_boundary_spline_accuracy(self, mode, order):
+        """Tests based on examples from gh-2640"""
+        data = np.arange(-6, 7, dtype=float)
+        x = np.linspace(-8, 15, num=1000)
+        y = ndimage.map_coordinates(data, [x], order=order, mode=mode)
+
+        # compute expected value using explicit padding via np.pad
+        npad = 32
+        pad_mode = ndimage_to_numpy_mode.get(mode)
+        padded = np.pad(data, npad, mode=pad_mode)
+        expected = ndimage.map_coordinates(padded, [npad + x], order=order,
+                                           mode=mode)
+
+        atol = 1e-5 if mode == 'grid-constant' else 1e-12
+        assert_allclose(y, expected, rtol=1e-7, atol=atol)
+
+    @pytest.mark.parametrize('order', range(2, 6))
+    @pytest.mark.parametrize('dtype', types)
+    def test_spline01(self, dtype, order):
+        data = np.ones([], dtype)
+        out = ndimage.spline_filter(data, order=order)
+        assert_array_almost_equal(out, 1)
+
+    @pytest.mark.parametrize('order', range(2, 6))
+    @pytest.mark.parametrize('dtype', types)
+    def test_spline02(self, dtype, order):
+        data = np.array([1], dtype)
+        out = ndimage.spline_filter(data, order=order)
+        assert_array_almost_equal(out, [1])
+
+    @pytest.mark.parametrize('order', range(2, 6))
+    @pytest.mark.parametrize('dtype', types)
+    def test_spline03(self, dtype, order):
+        data = np.ones([], dtype)
+        out = ndimage.spline_filter(data, order, output=dtype)
+        assert_array_almost_equal(out, 1)
+
+    @pytest.mark.parametrize('order', range(2, 6))
+    @pytest.mark.parametrize('dtype', types)
+    def test_spline04(self, dtype, order):
+        data = np.ones([4], dtype)
+        out = ndimage.spline_filter(data, order)
+        assert_array_almost_equal(out, [1, 1, 1, 1])
+
+    @pytest.mark.parametrize('order', range(2, 6))
+    @pytest.mark.parametrize('dtype', types)
+    def test_spline05(self, dtype, order):
+        data = np.ones([4, 4], dtype)
+        out = ndimage.spline_filter(data, order=order)
+        assert_array_almost_equal(out, [[1, 1, 1, 1],
+                                        [1, 1, 1, 1],
+                                        [1, 1, 1, 1],
+                                        [1, 1, 1, 1]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform01(self, order):
+        data = np.array([1])
+
+        def mapping(x):
+            return x
+
+        out = ndimage.geometric_transform(data, mapping, data.shape,
+                                          order=order)
+        assert_array_almost_equal(out, [1])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform02(self, order):
+        data = np.ones([4])
+
+        def mapping(x):
+            return x
+
+        out = ndimage.geometric_transform(data, mapping, data.shape,
+                                          order=order)
+        assert_array_almost_equal(out, [1, 1, 1, 1])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform03(self, order):
+        data = np.ones([4])
+
+        def mapping(x):
+            return (x[0] - 1,)
+
+        out = ndimage.geometric_transform(data, mapping, data.shape,
+                                          order=order)
+        assert_array_almost_equal(out, [0, 1, 1, 1])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform04(self, order):
+        data = np.array([4, 1, 3, 2])
+
+        def mapping(x):
+            return (x[0] - 1,)
+
+        out = ndimage.geometric_transform(data, mapping, data.shape,
+                                          order=order)
+        assert_array_almost_equal(out, [0, 4, 1, 3])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    @pytest.mark.parametrize('dtype', [np.float64, np.complex128])
+    def test_geometric_transform05(self, order, dtype):
+        data = np.array([[1, 1, 1, 1],
+                         [1, 1, 1, 1],
+                         [1, 1, 1, 1]], dtype=dtype)
+        expected = np.array([[0, 1, 1, 1],
+                             [0, 1, 1, 1],
+                             [0, 1, 1, 1]], dtype=dtype)
+        if data.dtype.kind == 'c':
+            data -= 1j * data
+            expected -= 1j * expected
+
+        def mapping(x):
+            return (x[0], x[1] - 1)
+
+        out = ndimage.geometric_transform(data, mapping, data.shape,
+                                          order=order)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform06(self, order):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+
+        def mapping(x):
+            return (x[0], x[1] - 1)
+
+        out = ndimage.geometric_transform(data, mapping, data.shape,
+                                          order=order)
+        assert_array_almost_equal(out, [[0, 4, 1, 3],
+                                        [0, 7, 6, 8],
+                                        [0, 3, 5, 3]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform07(self, order):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+
+        def mapping(x):
+            return (x[0] - 1, x[1])
+
+        out = ndimage.geometric_transform(data, mapping, data.shape,
+                                          order=order)
+        assert_array_almost_equal(out, [[0, 0, 0, 0],
+                                        [4, 1, 3, 2],
+                                        [7, 6, 8, 5]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform08(self, order):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+
+        def mapping(x):
+            return (x[0] - 1, x[1] - 1)
+
+        out = ndimage.geometric_transform(data, mapping, data.shape,
+                                          order=order)
+        assert_array_almost_equal(out, [[0, 0, 0, 0],
+                                        [0, 4, 1, 3],
+                                        [0, 7, 6, 8]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform10(self, order):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+
+        def mapping(x):
+            return (x[0] - 1, x[1] - 1)
+
+        if (order > 1):
+            filtered = ndimage.spline_filter(data, order=order)
+        else:
+            filtered = data
+        out = ndimage.geometric_transform(filtered, mapping, data.shape,
+                                          order=order, prefilter=False)
+        assert_array_almost_equal(out, [[0, 0, 0, 0],
+                                        [0, 4, 1, 3],
+                                        [0, 7, 6, 8]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform13(self, order):
+        data = np.ones([2], np.float64)
+
+        def mapping(x):
+            return (x[0] // 2,)
+
+        out = ndimage.geometric_transform(data, mapping, [4], order=order)
+        assert_array_almost_equal(out, [1, 1, 1, 1])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform14(self, order):
+        data = [1, 5, 2, 6, 3, 7, 4, 4]
+
+        def mapping(x):
+            return (2 * x[0],)
+
+        out = ndimage.geometric_transform(data, mapping, [4], order=order)
+        assert_array_almost_equal(out, [1, 2, 3, 4])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform15(self, order):
+        data = [1, 2, 3, 4]
+
+        def mapping(x):
+            return (x[0] / 2,)
+
+        out = ndimage.geometric_transform(data, mapping, [8], order=order)
+        assert_array_almost_equal(out[::2], [1, 2, 3, 4])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform16(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9.0, 10, 11, 12]]
+
+        def mapping(x):
+            return (x[0], x[1] * 2)
+
+        out = ndimage.geometric_transform(data, mapping, (3, 2),
+                                          order=order)
+        assert_array_almost_equal(out, [[1, 3], [5, 7], [9, 11]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform17(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+
+        def mapping(x):
+            return (x[0] * 2, x[1])
+
+        out = ndimage.geometric_transform(data, mapping, (1, 4),
+                                          order=order)
+        assert_array_almost_equal(out, [[1, 2, 3, 4]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform18(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+
+        def mapping(x):
+            return (x[0] * 2, x[1] * 2)
+
+        out = ndimage.geometric_transform(data, mapping, (1, 2),
+                                          order=order)
+        assert_array_almost_equal(out, [[1, 3]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform19(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+
+        def mapping(x):
+            return (x[0], x[1] / 2)
+
+        out = ndimage.geometric_transform(data, mapping, (3, 8),
+                                          order=order)
+        assert_array_almost_equal(out[..., ::2], data)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform20(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+
+        def mapping(x):
+            return (x[0] / 2, x[1])
+
+        out = ndimage.geometric_transform(data, mapping, (6, 4),
+                                          order=order)
+        assert_array_almost_equal(out[::2, ...], data)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform21(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+
+        def mapping(x):
+            return (x[0] / 2, x[1] / 2)
+
+        out = ndimage.geometric_transform(data, mapping, (6, 8),
+                                          order=order)
+        assert_array_almost_equal(out[::2, ::2], data)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform22(self, order):
+        data = np.array([[1, 2, 3, 4],
+                         [5, 6, 7, 8],
+                         [9, 10, 11, 12]], np.float64)
+
+        def mapping1(x):
+            return (x[0] / 2, x[1] / 2)
+
+        def mapping2(x):
+            return (x[0] * 2, x[1] * 2)
+
+        out = ndimage.geometric_transform(data, mapping1,
+                                          (6, 8), order=order)
+        out = ndimage.geometric_transform(out, mapping2,
+                                          (3, 4), order=order)
+        assert_array_almost_equal(out, data)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform23(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+
+        def mapping(x):
+            return (1, x[0] * 2)
+
+        out = ndimage.geometric_transform(data, mapping, (2,), order=order)
+        out = out.astype(np.int32)
+        assert_array_almost_equal(out, [5, 7])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_geometric_transform24(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+
+        def mapping(x, a, b):
+            return (a, x[0] * b)
+
+        out = ndimage.geometric_transform(
+            data, mapping, (2,), order=order, extra_arguments=(1,),
+            extra_keywords={'b': 2})
+        assert_array_almost_equal(out, [5, 7])
+
+    def test_geometric_transform_grid_constant_order1(self):
+        # verify interpolation outside the original bounds
+        x = np.array([[1, 2, 3],
+                      [4, 5, 6]], dtype=float)
+
+        def mapping(x):
+            return (x[0] - 0.5), (x[1] - 0.5)
+
+        expected_result = np.array([[0.25, 0.75, 1.25],
+                                    [1.25, 3.00, 4.00]])
+        assert_array_almost_equal(
+            ndimage.geometric_transform(x, mapping, mode='grid-constant',
+                                        order=1),
+            expected_result,
+        )
+
+    @pytest.mark.parametrize('mode', ['grid-constant', 'grid-wrap', 'nearest',
+                                      'mirror', 'reflect'])
+    @pytest.mark.parametrize('order', range(6))
+    def test_geometric_transform_vs_padded(self, order, mode):
+        x = np.arange(144, dtype=float).reshape(12, 12)
+
+        def mapping(x):
+            return (x[0] - 0.4), (x[1] + 2.3)
+
+        # Manually pad and then extract center after the transform to get the
+        # expected result.
+        npad = 24
+        pad_mode = ndimage_to_numpy_mode.get(mode)
+        xp = np.pad(x, npad, mode=pad_mode)
+        center_slice = tuple([slice(npad, -npad)] * x.ndim)
+        expected_result = ndimage.geometric_transform(
+            xp, mapping, mode=mode, order=order)[center_slice]
+
+        assert_allclose(
+            ndimage.geometric_transform(x, mapping, mode=mode,
+                                        order=order),
+            expected_result,
+            rtol=1e-7,
+        )
+
+    def test_geometric_transform_endianness_with_output_parameter(self):
+        # geometric transform given output ndarray or dtype with
+        # non-native endianness. see issue #4127
+        data = np.array([1])
+
+        def mapping(x):
+            return x
+
+        for out in [data.dtype, data.dtype.newbyteorder(),
+                    np.empty_like(data),
+                    np.empty_like(data).astype(data.dtype.newbyteorder())]:
+            returned = ndimage.geometric_transform(data, mapping, data.shape,
+                                                   output=out)
+            result = out if returned is None else returned
+            assert_array_almost_equal(result, [1])
+
+    def test_geometric_transform_with_string_output(self):
+        data = np.array([1])
+
+        def mapping(x):
+            return x
+
+        out = ndimage.geometric_transform(data, mapping, output='f')
+        assert_(out.dtype is np.dtype('f'))
+        assert_array_almost_equal(out, [1])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    @pytest.mark.parametrize('dtype', [np.float64, np.complex128])
+    def test_map_coordinates01(self, order, dtype):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+        expected = np.array([[0, 0, 0, 0],
+                             [0, 4, 1, 3],
+                             [0, 7, 6, 8]])
+        if data.dtype.kind == 'c':
+            data = data - 1j * data
+            expected = expected - 1j * expected
+
+        idx = np.indices(data.shape)
+        idx -= 1
+
+        out = ndimage.map_coordinates(data, idx, order=order)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_map_coordinates02(self, order):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+        idx = np.indices(data.shape, np.float64)
+        idx -= 0.5
+
+        out1 = ndimage.shift(data, 0.5, order=order)
+        out2 = ndimage.map_coordinates(data, idx, order=order)
+        assert_array_almost_equal(out1, out2)
+
+    def test_map_coordinates03(self):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]], order='F')
+        idx = np.indices(data.shape) - 1
+        out = ndimage.map_coordinates(data, idx)
+        assert_array_almost_equal(out, [[0, 0, 0, 0],
+                                        [0, 4, 1, 3],
+                                        [0, 7, 6, 8]])
+        assert_array_almost_equal(out, ndimage.shift(data, (1, 1)))
+        idx = np.indices(data[::2].shape) - 1
+        out = ndimage.map_coordinates(data[::2], idx)
+        assert_array_almost_equal(out, [[0, 0, 0, 0],
+                                        [0, 4, 1, 3]])
+        assert_array_almost_equal(out, ndimage.shift(data[::2], (1, 1)))
+        idx = np.indices(data[:, ::2].shape) - 1
+        out = ndimage.map_coordinates(data[:, ::2], idx)
+        assert_array_almost_equal(out, [[0, 0], [0, 4], [0, 7]])
+        assert_array_almost_equal(out, ndimage.shift(data[:, ::2], (1, 1)))
+
+    def test_map_coordinates_endianness_with_output_parameter(self):
+        # output parameter given as array or dtype with either endianness
+        # see issue #4127
+        data = np.array([[1, 2], [7, 6]])
+        expected = np.array([[0, 0], [0, 1]])
+        idx = np.indices(data.shape)
+        idx -= 1
+        for out in [
+            data.dtype,
+            data.dtype.newbyteorder(),
+            np.empty_like(expected),
+            np.empty_like(expected).astype(expected.dtype.newbyteorder())
+        ]:
+            returned = ndimage.map_coordinates(data, idx, output=out)
+            result = out if returned is None else returned
+            assert_array_almost_equal(result, expected)
+
+    def test_map_coordinates_with_string_output(self):
+        data = np.array([[1]])
+        idx = np.indices(data.shape)
+        out = ndimage.map_coordinates(data, idx, output='f')
+        assert_(out.dtype is np.dtype('f'))
+        assert_array_almost_equal(out, [[1]])
+
+    @pytest.mark.skipif('win32' in sys.platform or np.intp(0).itemsize < 8,
+                        reason='do not run on 32 bit or windows '
+                               '(no sparse memory)')
+    def test_map_coordinates_large_data(self):
+        # check crash on large data
+        try:
+            n = 30000
+            a = np.empty(n**2, dtype=np.float32).reshape(n, n)
+            # fill the part we might read
+            a[n - 3:, n - 3:] = 0
+            ndimage.map_coordinates(a, [[n - 1.5], [n - 1.5]], order=1)
+        except MemoryError as e:
+            raise pytest.skip('Not enough memory available') from e
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform01(self, order):
+        data = np.array([1])
+        out = ndimage.affine_transform(data, [[1]], order=order)
+        assert_array_almost_equal(out, [1])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform02(self, order):
+        data = np.ones([4])
+        out = ndimage.affine_transform(data, [[1]], order=order)
+        assert_array_almost_equal(out, [1, 1, 1, 1])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform03(self, order):
+        data = np.ones([4])
+        out = ndimage.affine_transform(data, [[1]], -1, order=order)
+        assert_array_almost_equal(out, [0, 1, 1, 1])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform04(self, order):
+        data = np.array([4, 1, 3, 2])
+        out = ndimage.affine_transform(data, [[1]], -1, order=order)
+        assert_array_almost_equal(out, [0, 4, 1, 3])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    @pytest.mark.parametrize('dtype', [np.float64, np.complex128])
+    def test_affine_transform05(self, order, dtype):
+        data = np.array([[1, 1, 1, 1],
+                         [1, 1, 1, 1],
+                         [1, 1, 1, 1]], dtype=dtype)
+        expected = np.array([[0, 1, 1, 1],
+                             [0, 1, 1, 1],
+                             [0, 1, 1, 1]], dtype=dtype)
+        if data.dtype.kind == 'c':
+            data -= 1j * data
+            expected -= 1j * expected
+        out = ndimage.affine_transform(data, [[1, 0], [0, 1]],
+                                       [0, -1], order=order)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform06(self, order):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+        out = ndimage.affine_transform(data, [[1, 0], [0, 1]],
+                                       [0, -1], order=order)
+        assert_array_almost_equal(out, [[0, 4, 1, 3],
+                                        [0, 7, 6, 8],
+                                        [0, 3, 5, 3]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform07(self, order):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+        out = ndimage.affine_transform(data, [[1, 0], [0, 1]],
+                                       [-1, 0], order=order)
+        assert_array_almost_equal(out, [[0, 0, 0, 0],
+                                        [4, 1, 3, 2],
+                                        [7, 6, 8, 5]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform08(self, order):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+        out = ndimage.affine_transform(data, [[1, 0], [0, 1]],
+                                       [-1, -1], order=order)
+        assert_array_almost_equal(out, [[0, 0, 0, 0],
+                                        [0, 4, 1, 3],
+                                        [0, 7, 6, 8]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform09(self, order):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+        if (order > 1):
+            filtered = ndimage.spline_filter(data, order=order)
+        else:
+            filtered = data
+        out = ndimage.affine_transform(filtered, [[1, 0], [0, 1]],
+                                       [-1, -1], order=order,
+                                       prefilter=False)
+        assert_array_almost_equal(out, [[0, 0, 0, 0],
+                                        [0, 4, 1, 3],
+                                        [0, 7, 6, 8]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform10(self, order):
+        data = np.ones([2], np.float64)
+        out = ndimage.affine_transform(data, [[0.5]], output_shape=(4,),
+                                       order=order)
+        assert_array_almost_equal(out, [1, 1, 1, 0])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform11(self, order):
+        data = [1, 5, 2, 6, 3, 7, 4, 4]
+        out = ndimage.affine_transform(data, [[2]], 0, (4,), order=order)
+        assert_array_almost_equal(out, [1, 2, 3, 4])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform12(self, order):
+        data = [1, 2, 3, 4]
+        out = ndimage.affine_transform(data, [[0.5]], 0, (8,), order=order)
+        assert_array_almost_equal(out[::2], [1, 2, 3, 4])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform13(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9.0, 10, 11, 12]]
+        out = ndimage.affine_transform(data, [[1, 0], [0, 2]], 0, (3, 2),
+                                       order=order)
+        assert_array_almost_equal(out, [[1, 3], [5, 7], [9, 11]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform14(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+        out = ndimage.affine_transform(data, [[2, 0], [0, 1]], 0, (1, 4),
+                                       order=order)
+        assert_array_almost_equal(out, [[1, 2, 3, 4]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform15(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+        out = ndimage.affine_transform(data, [[2, 0], [0, 2]], 0, (1, 2),
+                                       order=order)
+        assert_array_almost_equal(out, [[1, 3]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform16(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+        out = ndimage.affine_transform(data, [[1, 0.0], [0, 0.5]], 0,
+                                       (3, 8), order=order)
+        assert_array_almost_equal(out[..., ::2], data)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform17(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+        out = ndimage.affine_transform(data, [[0.5, 0], [0, 1]], 0,
+                                       (6, 4), order=order)
+        assert_array_almost_equal(out[::2, ...], data)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform18(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+        out = ndimage.affine_transform(data, [[0.5, 0], [0, 0.5]], 0,
+                                       (6, 8), order=order)
+        assert_array_almost_equal(out[::2, ::2], data)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform19(self, order):
+        data = np.array([[1, 2, 3, 4],
+                         [5, 6, 7, 8],
+                         [9, 10, 11, 12]], np.float64)
+        out = ndimage.affine_transform(data, [[0.5, 0], [0, 0.5]], 0,
+                                       (6, 8), order=order)
+        out = ndimage.affine_transform(out, [[2.0, 0], [0, 2.0]], 0,
+                                       (3, 4), order=order)
+        assert_array_almost_equal(out, data)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform20(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+        out = ndimage.affine_transform(data, [[0], [2]], 0, (2,),
+                                       order=order)
+        assert_array_almost_equal(out, [1, 3])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform21(self, order):
+        data = [[1, 2, 3, 4],
+                [5, 6, 7, 8],
+                [9, 10, 11, 12]]
+        out = ndimage.affine_transform(data, [[2], [0]], 0, (2,),
+                                       order=order)
+        assert_array_almost_equal(out, [1, 9])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform22(self, order):
+        # shift and offset interaction; see issue #1547
+        data = np.array([4, 1, 3, 2])
+        out = ndimage.affine_transform(data, [[2]], [-1], (3,),
+                                       order=order)
+        assert_array_almost_equal(out, [0, 1, 2])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform23(self, order):
+        # shift and offset interaction; see issue #1547
+        data = np.array([4, 1, 3, 2])
+        out = ndimage.affine_transform(data, [[0.5]], [-1], (8,),
+                                       order=order)
+        assert_array_almost_equal(out[::2], [0, 4, 1, 3])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform24(self, order):
+        # consistency between diagonal and non-diagonal case; see issue #1547
+        data = np.array([4, 1, 3, 2])
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning,
+                       'The behavior of affine_transform with a 1-D array .* '
+                       'has changed')
+            out1 = ndimage.affine_transform(data, [2], -1, order=order)
+        out2 = ndimage.affine_transform(data, [[2]], -1, order=order)
+        assert_array_almost_equal(out1, out2)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform25(self, order):
+        # consistency between diagonal and non-diagonal case; see issue #1547
+        data = np.array([4, 1, 3, 2])
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning,
+                       'The behavior of affine_transform with a 1-D array .* '
+                       'has changed')
+            out1 = ndimage.affine_transform(data, [0.5], -1, order=order)
+        out2 = ndimage.affine_transform(data, [[0.5]], -1, order=order)
+        assert_array_almost_equal(out1, out2)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform26(self, order):
+        # test homogeneous coordinates
+        data = np.array([[4, 1, 3, 2],
+                            [7, 6, 8, 5],
+                            [3, 5, 3, 6]])
+        if (order > 1):
+            filtered = ndimage.spline_filter(data, order=order)
+        else:
+            filtered = data
+        tform_original = np.eye(2)
+        offset_original = -np.ones((2, 1))
+        tform_h1 = np.hstack((tform_original, offset_original))
+        tform_h2 = np.vstack((tform_h1, [[0, 0, 1]]))
+        out1 = ndimage.affine_transform(filtered, tform_original,
+                                        offset_original.ravel(),
+                                        order=order, prefilter=False)
+        out2 = ndimage.affine_transform(filtered, tform_h1, order=order,
+                                        prefilter=False)
+        out3 = ndimage.affine_transform(filtered, tform_h2, order=order,
+                                        prefilter=False)
+        for out in [out1, out2, out3]:
+            assert_array_almost_equal(out, [[0, 0, 0, 0],
+                                            [0, 4, 1, 3],
+                                            [0, 7, 6, 8]])
+
+    def test_affine_transform27(self):
+        # test valid homogeneous transformation matrix
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+        tform_h1 = np.hstack((np.eye(2), -np.ones((2, 1))))
+        tform_h2 = np.vstack((tform_h1, [[5, 2, 1]]))
+        assert_raises(ValueError, ndimage.affine_transform, data, tform_h2)
+
+    def test_affine_transform_1d_endianness_with_output_parameter(self):
+        # 1d affine transform given output ndarray or dtype with
+        # either endianness. see issue #7388
+        data = np.ones((2, 2))
+        for out in [np.empty_like(data),
+                    np.empty_like(data).astype(data.dtype.newbyteorder()),
+                    data.dtype, data.dtype.newbyteorder()]:
+            with suppress_warnings() as sup:
+                sup.filter(UserWarning,
+                           'The behavior of affine_transform with a 1-D array '
+                           '.* has changed')
+                returned = ndimage.affine_transform(data, [1, 1], output=out)
+            result = out if returned is None else returned
+            assert_array_almost_equal(result, [[1, 1], [1, 1]])
+
+    def test_affine_transform_multi_d_endianness_with_output_parameter(self):
+        # affine transform given output ndarray or dtype with either endianness
+        # see issue #4127
+        data = np.array([1])
+        for out in [data.dtype, data.dtype.newbyteorder(),
+                    np.empty_like(data),
+                    np.empty_like(data).astype(data.dtype.newbyteorder())]:
+            returned = ndimage.affine_transform(data, [[1]], output=out)
+            result = out if returned is None else returned
+            assert_array_almost_equal(result, [1])
+
+    def test_affine_transform_output_shape(self):
+        # don't require output_shape when out of a different size is given
+        data = np.arange(8, dtype=np.float64)
+        out = np.ones((16,))
+
+        ndimage.affine_transform(data, [[1]], output=out)
+        assert_array_almost_equal(out[:8], data)
+
+        # mismatched output shape raises an error
+        with pytest.raises(RuntimeError):
+            ndimage.affine_transform(
+                data, [[1]], output=out, output_shape=(12,))
+
+    def test_affine_transform_with_string_output(self):
+        data = np.array([1])
+        out = ndimage.affine_transform(data, [[1]], output='f')
+        assert_(out.dtype is np.dtype('f'))
+        assert_array_almost_equal(out, [1])
+
+    @pytest.mark.parametrize('shift',
+                             [(1, 0), (0, 1), (-1, 1), (3, -5), (2, 7)])
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform_shift_via_grid_wrap(self, shift, order):
+        # For mode 'grid-wrap', integer shifts should match np.roll
+        x = np.array([[0, 1],
+                      [2, 3]])
+        affine = np.zeros((2, 3))
+        affine[:2, :2] = np.eye(2)
+        affine[:, 2] = shift
+        assert_array_almost_equal(
+            ndimage.affine_transform(x, affine, mode='grid-wrap', order=order),
+            np.roll(x, shift, axis=(0, 1)),
+        )
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_affine_transform_shift_reflect(self, order):
+        # shift by x.shape results in reflection
+        x = np.array([[0, 1, 2],
+                      [3, 4, 5]])
+        affine = np.zeros((2, 3))
+        affine[:2, :2] = np.eye(2)
+        affine[:, 2] = x.shape
+        assert_array_almost_equal(
+            ndimage.affine_transform(x, affine, mode='reflect', order=order),
+            x[::-1, ::-1],
+        )
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_shift01(self, order):
+        data = np.array([1])
+        out = ndimage.shift(data, [1], order=order)
+        assert_array_almost_equal(out, [0])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_shift02(self, order):
+        data = np.ones([4])
+        out = ndimage.shift(data, [1], order=order)
+        assert_array_almost_equal(out, [0, 1, 1, 1])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_shift03(self, order):
+        data = np.ones([4])
+        out = ndimage.shift(data, -1, order=order)
+        assert_array_almost_equal(out, [1, 1, 1, 0])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_shift04(self, order):
+        data = np.array([4, 1, 3, 2])
+        out = ndimage.shift(data, 1, order=order)
+        assert_array_almost_equal(out, [0, 4, 1, 3])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    @pytest.mark.parametrize('dtype', [np.float64, np.complex128])
+    def test_shift05(self, order, dtype):
+        data = np.array([[1, 1, 1, 1],
+                         [1, 1, 1, 1],
+                         [1, 1, 1, 1]], dtype=dtype)
+        expected = np.array([[0, 1, 1, 1],
+                             [0, 1, 1, 1],
+                             [0, 1, 1, 1]], dtype=dtype)
+        if data.dtype.kind == 'c':
+            data -= 1j * data
+            expected -= 1j * expected
+        out = ndimage.shift(data, [0, 1], order=order)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    @pytest.mark.parametrize('mode', ['constant', 'grid-constant'])
+    @pytest.mark.parametrize('dtype', [np.float64, np.complex128])
+    def test_shift_with_nonzero_cval(self, order, mode, dtype):
+        data = np.array([[1, 1, 1, 1],
+                         [1, 1, 1, 1],
+                         [1, 1, 1, 1]], dtype=dtype)
+
+        expected = np.array([[0, 1, 1, 1],
+                             [0, 1, 1, 1],
+                             [0, 1, 1, 1]], dtype=dtype)
+
+        if data.dtype.kind == 'c':
+            data -= 1j * data
+            expected -= 1j * expected
+        cval = 5.0
+        expected[:, 0] = cval  # specific to shift of [0, 1] used below
+        out = ndimage.shift(data, [0, 1], order=order, mode=mode, cval=cval)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_shift06(self, order):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+        out = ndimage.shift(data, [0, 1], order=order)
+        assert_array_almost_equal(out, [[0, 4, 1, 3],
+                                        [0, 7, 6, 8],
+                                        [0, 3, 5, 3]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_shift07(self, order):
+        data = np.array([[4, 1, 3, 2],
+                            [7, 6, 8, 5],
+                            [3, 5, 3, 6]])
+        out = ndimage.shift(data, [1, 0], order=order)
+        assert_array_almost_equal(out, [[0, 0, 0, 0],
+                                        [4, 1, 3, 2],
+                                        [7, 6, 8, 5]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_shift08(self, order):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+        out = ndimage.shift(data, [1, 1], order=order)
+        assert_array_almost_equal(out, [[0, 0, 0, 0],
+                                        [0, 4, 1, 3],
+                                        [0, 7, 6, 8]])
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_shift09(self, order):
+        data = np.array([[4, 1, 3, 2],
+                         [7, 6, 8, 5],
+                         [3, 5, 3, 6]])
+        if (order > 1):
+            filtered = ndimage.spline_filter(data, order=order)
+        else:
+            filtered = data
+        out = ndimage.shift(filtered, [1, 1], order=order, prefilter=False)
+        assert_array_almost_equal(out, [[0, 0, 0, 0],
+                                        [0, 4, 1, 3],
+                                        [0, 7, 6, 8]])
+
+    @pytest.mark.parametrize('shift',
+                             [(1, 0), (0, 1), (-1, 1), (3, -5), (2, 7)])
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_shift_grid_wrap(self, shift, order):
+        # For mode 'grid-wrap', integer shifts should match np.roll
+        x = np.array([[0, 1],
+                      [2, 3]])
+        assert_array_almost_equal(
+            ndimage.shift(x, shift, mode='grid-wrap', order=order),
+            np.roll(x, shift, axis=(0, 1)),
+        )
+
+    @pytest.mark.parametrize('shift',
+                             [(1, 0), (0, 1), (-1, 1), (3, -5), (2, 7)])
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_shift_grid_constant1(self, shift, order):
+        # For integer shifts, 'constant' and 'grid-constant' should be equal
+        x = np.arange(20).reshape((5, 4))
+        assert_array_almost_equal(
+            ndimage.shift(x, shift, mode='grid-constant', order=order),
+            ndimage.shift(x, shift, mode='constant', order=order),
+        )
+
+    def test_shift_grid_constant_order1(self):
+        x = np.array([[1, 2, 3],
+                      [4, 5, 6]], dtype=float)
+        expected_result = np.array([[0.25, 0.75, 1.25],
+                                    [1.25, 3.00, 4.00]])
+        assert_array_almost_equal(
+            ndimage.shift(x, (0.5, 0.5), mode='grid-constant', order=1),
+            expected_result,
+        )
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_shift_reflect(self, order):
+        # shift by x.shape results in reflection
+        x = np.array([[0, 1, 2],
+                      [3, 4, 5]])
+        assert_array_almost_equal(
+            ndimage.shift(x, x.shape, mode='reflect', order=order),
+            x[::-1, ::-1],
+        )
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    @pytest.mark.parametrize('prefilter', [False, True])
+    def test_shift_nearest_boundary(self, order, prefilter):
+        # verify that shifting at least order // 2 beyond the end of the array
+        # gives a value equal to the edge value.
+        x = np.arange(16)
+        kwargs = dict(mode='nearest', order=order, prefilter=prefilter)
+        assert_array_almost_equal(
+            ndimage.shift(x, order // 2 + 1, **kwargs)[0], x[0],
+        )
+        assert_array_almost_equal(
+            ndimage.shift(x, -order // 2 - 1, **kwargs)[-1], x[-1],
+        )
+
+    @pytest.mark.parametrize('mode', ['grid-constant', 'grid-wrap', 'nearest',
+                                      'mirror', 'reflect'])
+    @pytest.mark.parametrize('order', range(6))
+    def test_shift_vs_padded(self, order, mode):
+        x = np.arange(144, dtype=float).reshape(12, 12)
+        shift = (0.4, -2.3)
+
+        # manually pad and then extract center to get expected result
+        npad = 32
+        pad_mode = ndimage_to_numpy_mode.get(mode)
+        xp = np.pad(x, npad, mode=pad_mode)
+        center_slice = tuple([slice(npad, -npad)] * x.ndim)
+        expected_result = ndimage.shift(
+            xp, shift, mode=mode, order=order)[center_slice]
+
+        assert_allclose(
+            ndimage.shift(x, shift, mode=mode, order=order),
+            expected_result,
+            rtol=1e-7,
+        )
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_zoom1(self, order):
+        for z in [2, [2, 2]]:
+            arr = np.array(list(range(25))).reshape((5, 5)).astype(float)
+            arr = ndimage.zoom(arr, z, order=order)
+            assert_equal(arr.shape, (10, 10))
+            assert_(np.all(arr[-1, :] != 0))
+            assert_(np.all(arr[-1, :] >= (20 - eps)))
+            assert_(np.all(arr[0, :] <= (5 + eps)))
+            assert_(np.all(arr >= (0 - eps)))
+            assert_(np.all(arr <= (24 + eps)))
+
+    def test_zoom2(self):
+        arr = np.arange(12).reshape((3, 4))
+        out = ndimage.zoom(ndimage.zoom(arr, 2), 0.5)
+        assert_array_equal(out, arr)
+
+    def test_zoom3(self):
+        arr = np.array([[1, 2]])
+        out1 = ndimage.zoom(arr, (2, 1))
+        out2 = ndimage.zoom(arr, (1, 2))
+
+        assert_array_almost_equal(out1, np.array([[1, 2], [1, 2]]))
+        assert_array_almost_equal(out2, np.array([[1, 1, 2, 2]]))
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    @pytest.mark.parametrize('dtype', [np.float64, np.complex128])
+    def test_zoom_affine01(self, order, dtype):
+        data = np.asarray([[1, 2, 3, 4],
+                              [5, 6, 7, 8],
+                              [9, 10, 11, 12]], dtype=dtype)
+        if data.dtype.kind == 'c':
+            data -= 1j * data
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning,
+                       'The behavior of affine_transform with a 1-D array .* '
+                       'has changed')
+            out = ndimage.affine_transform(data, [0.5, 0.5], 0,
+                                           (6, 8), order=order)
+        assert_array_almost_equal(out[::2, ::2], data)
+
+    def test_zoom_infinity(self):
+        # Ticket #1419 regression test
+        dim = 8
+        ndimage.zoom(np.zeros((dim, dim)), 1. / dim, mode='nearest')
+
+    def test_zoom_zoomfactor_one(self):
+        # Ticket #1122 regression test
+        arr = np.zeros((1, 5, 5))
+        zoom = (1.0, 2.0, 2.0)
+
+        out = ndimage.zoom(arr, zoom, cval=7)
+        ref = np.zeros((1, 10, 10))
+        assert_array_almost_equal(out, ref)
+
+    def test_zoom_output_shape_roundoff(self):
+        arr = np.zeros((3, 11, 25))
+        zoom = (4.0 / 3, 15.0 / 11, 29.0 / 25)
+        out = ndimage.zoom(arr, zoom)
+        assert_array_equal(out.shape, (4, 15, 29))
+
+    @pytest.mark.parametrize('zoom', [(1, 1), (3, 5), (8, 2), (8, 8)])
+    @pytest.mark.parametrize('mode', ['nearest', 'constant', 'wrap', 'reflect',
+                                      'mirror', 'grid-wrap', 'grid-mirror',
+                                      'grid-constant'])
+    def test_zoom_by_int_order0(self, zoom, mode):
+        # order 0 zoom should be the same as replication via np.kron
+        # Note: This is not True for general x shapes when grid_mode is False,
+        #       but works here for all modes because the size ratio happens to
+        #       always be an integer when x.shape = (2, 2).
+        x = np.array([[0, 1],
+                         [2, 3]], dtype=float)
+        # x = np.arange(16, dtype=float).reshape(4, 4)
+        assert_array_almost_equal(
+            ndimage.zoom(x, zoom, order=0, mode=mode),
+            np.kron(x, np.ones(zoom))
+        )
+
+    @pytest.mark.parametrize('shape', [(2, 3), (4, 4)])
+    @pytest.mark.parametrize('zoom', [(1, 1), (3, 5), (8, 2), (8, 8)])
+    @pytest.mark.parametrize('mode', ['nearest', 'reflect', 'mirror',
+                                      'grid-wrap', 'grid-constant'])
+    def test_zoom_grid_by_int_order0(self, shape, zoom, mode):
+        # When grid_mode is True,  order 0 zoom should be the same as
+        # replication via np.kron. The only exceptions to this are the
+        # non-grid modes 'constant' and 'wrap'.
+        x = np.arange(np.prod(shape), dtype=float).reshape(shape)
+        assert_array_almost_equal(
+            ndimage.zoom(x, zoom, order=0, mode=mode, grid_mode=True),
+            np.kron(x, np.ones(zoom))
+        )
+
+    @pytest.mark.parametrize('mode', ['constant', 'wrap'])
+    def test_zoom_grid_mode_warnings(self, mode):
+        # Warn on use of non-grid modes when grid_mode is True
+        x = np.arange(9, dtype=float).reshape((3, 3))
+        with pytest.warns(UserWarning,
+                          match="It is recommended to use mode"):
+            ndimage.zoom(x, 2, mode=mode, grid_mode=True),
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_rotate01(self, order):
+        data = np.array([[0, 0, 0, 0],
+                         [0, 1, 1, 0],
+                         [0, 0, 0, 0]], dtype=np.float64)
+        out = ndimage.rotate(data, 0, order=order)
+        assert_array_almost_equal(out, data)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_rotate02(self, order):
+        data = np.array([[0, 0, 0, 0],
+                         [0, 1, 0, 0],
+                         [0, 0, 0, 0]], dtype=np.float64)
+        expected = np.array([[0, 0, 0],
+                            [0, 0, 0],
+                            [0, 1, 0],
+                            [0, 0, 0]], dtype=np.float64)
+        out = ndimage.rotate(data, 90, order=order)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    @pytest.mark.parametrize('dtype', [np.float64, np.complex128])
+    def test_rotate03(self, order, dtype):
+        data = np.array([[0, 0, 0, 0, 0],
+                         [0, 1, 1, 0, 0],
+                         [0, 0, 0, 0, 0]], dtype=dtype)
+        expected = np.array([[0, 0, 0],
+                            [0, 0, 0],
+                            [0, 1, 0],
+                            [0, 1, 0],
+                            [0, 0, 0]], dtype=dtype)
+        if data.dtype.kind == 'c':
+            data -= 1j * data
+            expected -= 1j * expected
+        out = ndimage.rotate(data, 90, order=order)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_rotate04(self, order):
+        data = np.array([[0, 0, 0, 0, 0],
+                         [0, 1, 1, 0, 0],
+                         [0, 0, 0, 0, 0]], dtype=np.float64)
+        expected = np.array([[0, 0, 0, 0, 0],
+                             [0, 0, 1, 0, 0],
+                             [0, 0, 1, 0, 0]], dtype=np.float64)
+        out = ndimage.rotate(data, 90, reshape=False, order=order)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_rotate05(self, order):
+        data = np.empty((4, 3, 3))
+        for i in range(3):
+            data[:, :, i] = np.array([[0, 0, 0],
+                                      [0, 1, 0],
+                                      [0, 1, 0],
+                                      [0, 0, 0]], dtype=np.float64)
+        expected = np.array([[0, 0, 0, 0],
+                             [0, 1, 1, 0],
+                             [0, 0, 0, 0]], dtype=np.float64)
+        out = ndimage.rotate(data, 90, order=order)
+        for i in range(3):
+            assert_array_almost_equal(out[:, :, i], expected)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_rotate06(self, order):
+        data = np.empty((3, 4, 3))
+        for i in range(3):
+            data[:, :, i] = np.array([[0, 0, 0, 0],
+                                      [0, 1, 1, 0],
+                                      [0, 0, 0, 0]], dtype=np.float64)
+        expected = np.array([[0, 0, 0],
+                             [0, 1, 0],
+                             [0, 1, 0],
+                             [0, 0, 0]], dtype=np.float64)
+        out = ndimage.rotate(data, 90, order=order)
+        for i in range(3):
+            assert_array_almost_equal(out[:, :, i], expected)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_rotate07(self, order):
+        data = np.array([[[0, 0, 0, 0, 0],
+                          [0, 1, 1, 0, 0],
+                          [0, 0, 0, 0, 0]]] * 2, dtype=np.float64)
+        data = data.transpose()
+        expected = np.array([[[0, 0, 0],
+                              [0, 1, 0],
+                              [0, 1, 0],
+                              [0, 0, 0],
+                              [0, 0, 0]]] * 2, dtype=np.float64)
+        expected = expected.transpose([2, 1, 0])
+        out = ndimage.rotate(data, 90, axes=(0, 1), order=order)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('order', range(0, 6))
+    def test_rotate08(self, order):
+        data = np.array([[[0, 0, 0, 0, 0],
+                          [0, 1, 1, 0, 0],
+                          [0, 0, 0, 0, 0]]] * 2, dtype=np.float64)
+        data = data.transpose()
+        expected = np.array([[[0, 0, 1, 0, 0],
+                              [0, 0, 1, 0, 0],
+                              [0, 0, 0, 0, 0]]] * 2, dtype=np.float64)
+        expected = expected.transpose()
+        out = ndimage.rotate(data, 90, axes=(0, 1), reshape=False, order=order)
+        assert_array_almost_equal(out, expected)
+
+    def test_rotate09(self):
+        data = np.array([[0, 0, 0, 0, 0],
+                         [0, 1, 1, 0, 0],
+                         [0, 0, 0, 0, 0]] * 2, dtype=np.float64)
+        with assert_raises(ValueError):
+            ndimage.rotate(data, 90, axes=(0, data.ndim))
+
+    def test_rotate10(self):
+        data = np.arange(45, dtype=np.float64).reshape((3, 5, 3))
+
+        # The output of ndimage.rotate before refactoring
+        expected = np.array([[[0.0, 0.0, 0.0],
+                              [0.0, 0.0, 0.0],
+                              [6.54914793, 7.54914793, 8.54914793],
+                              [10.84520162, 11.84520162, 12.84520162],
+                              [0.0, 0.0, 0.0]],
+                             [[6.19286575, 7.19286575, 8.19286575],
+                              [13.4730712, 14.4730712, 15.4730712],
+                              [21.0, 22.0, 23.0],
+                              [28.5269288, 29.5269288, 30.5269288],
+                              [35.80713425, 36.80713425, 37.80713425]],
+                             [[0.0, 0.0, 0.0],
+                              [31.15479838, 32.15479838, 33.15479838],
+                              [35.45085207, 36.45085207, 37.45085207],
+                              [0.0, 0.0, 0.0],
+                              [0.0, 0.0, 0.0]]])
+
+        out = ndimage.rotate(data, angle=12, reshape=False)
+        assert_array_almost_equal(out, expected)
+
+    def test_rotate_exact_180(self):
+        a = np.tile(np.arange(5), (5, 1))
+        b = ndimage.rotate(ndimage.rotate(a, 180), -180)
+        assert_equal(a, b)
+
+
+def test_zoom_output_shape():
+    """Ticket #643"""
+    x = np.arange(12).reshape((3, 4))
+    ndimage.zoom(x, 2, output=np.zeros((6, 8)))
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_measurements.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_measurements.py
new file mode 100644
index 0000000000000000000000000000000000000000..a55b1a6014348ba022f9982900a3cf5e1bcf62af
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_measurements.py
@@ -0,0 +1,1419 @@
+import os.path
+
+import numpy as np
+from numpy.testing import (
+    assert_,
+    assert_allclose,
+    assert_almost_equal,
+    assert_array_almost_equal,
+    assert_array_equal,
+    assert_equal,
+    suppress_warnings,
+)
+import pytest
+from pytest import raises as assert_raises
+
+import scipy.ndimage as ndimage
+
+
+from . import types
+
+
+class Test_measurements_stats:
+    """ndimage._measurements._stats() is a utility used by other functions."""
+
+    def test_a(self):
+        x = [0, 1, 2, 6]
+        labels = [0, 0, 1, 1]
+        index = [0, 1]
+        for shp in [(4,), (2, 2)]:
+            x = np.array(x).reshape(shp)
+            labels = np.array(labels).reshape(shp)
+            counts, sums = ndimage._measurements._stats(
+                x, labels=labels, index=index)
+            assert_array_equal(counts, [2, 2])
+            assert_array_equal(sums, [1.0, 8.0])
+
+    def test_b(self):
+        # Same data as test_a, but different labels.  The label 9 exceeds the
+        # length of 'labels', so this test will follow a different code path.
+        x = [0, 1, 2, 6]
+        labels = [0, 0, 9, 9]
+        index = [0, 9]
+        for shp in [(4,), (2, 2)]:
+            x = np.array(x).reshape(shp)
+            labels = np.array(labels).reshape(shp)
+            counts, sums = ndimage._measurements._stats(
+                x, labels=labels, index=index)
+            assert_array_equal(counts, [2, 2])
+            assert_array_equal(sums, [1.0, 8.0])
+
+    def test_a_centered(self):
+        x = [0, 1, 2, 6]
+        labels = [0, 0, 1, 1]
+        index = [0, 1]
+        for shp in [(4,), (2, 2)]:
+            x = np.array(x).reshape(shp)
+            labels = np.array(labels).reshape(shp)
+            counts, sums, centers = ndimage._measurements._stats(
+                x, labels=labels, index=index, centered=True)
+            assert_array_equal(counts, [2, 2])
+            assert_array_equal(sums, [1.0, 8.0])
+            assert_array_equal(centers, [0.5, 8.0])
+
+    def test_b_centered(self):
+        x = [0, 1, 2, 6]
+        labels = [0, 0, 9, 9]
+        index = [0, 9]
+        for shp in [(4,), (2, 2)]:
+            x = np.array(x).reshape(shp)
+            labels = np.array(labels).reshape(shp)
+            counts, sums, centers = ndimage._measurements._stats(
+                x, labels=labels, index=index, centered=True)
+            assert_array_equal(counts, [2, 2])
+            assert_array_equal(sums, [1.0, 8.0])
+            assert_array_equal(centers, [0.5, 8.0])
+
+    def test_nonint_labels(self):
+        x = [0, 1, 2, 6]
+        labels = [0.0, 0.0, 9.0, 9.0]
+        index = [0.0, 9.0]
+        for shp in [(4,), (2, 2)]:
+            x = np.array(x).reshape(shp)
+            labels = np.array(labels).reshape(shp)
+            counts, sums, centers = ndimage._measurements._stats(
+                x, labels=labels, index=index, centered=True)
+            assert_array_equal(counts, [2, 2])
+            assert_array_equal(sums, [1.0, 8.0])
+            assert_array_equal(centers, [0.5, 8.0])
+
+
+class Test_measurements_select:
+    """ndimage._measurements._select() is a utility used by other functions."""
+
+    def test_basic(self):
+        x = [0, 1, 6, 2]
+        cases = [
+            ([0, 0, 1, 1], [0, 1]),           # "Small" integer labels
+            ([0, 0, 9, 9], [0, 9]),           # A label larger than len(labels)
+            ([0.0, 0.0, 7.0, 7.0], [0.0, 7.0]),   # Non-integer labels
+        ]
+        for labels, index in cases:
+            result = ndimage._measurements._select(
+                x, labels=labels, index=index)
+            assert_(len(result) == 0)
+            result = ndimage._measurements._select(
+                x, labels=labels, index=index, find_max=True)
+            assert_(len(result) == 1)
+            assert_array_equal(result[0], [1, 6])
+            result = ndimage._measurements._select(
+                x, labels=labels, index=index, find_min=True)
+            assert_(len(result) == 1)
+            assert_array_equal(result[0], [0, 2])
+            result = ndimage._measurements._select(
+                x, labels=labels, index=index, find_min=True,
+                find_min_positions=True)
+            assert_(len(result) == 2)
+            assert_array_equal(result[0], [0, 2])
+            assert_array_equal(result[1], [0, 3])
+            assert_equal(result[1].dtype.kind, 'i')
+            result = ndimage._measurements._select(
+                x, labels=labels, index=index, find_max=True,
+                find_max_positions=True)
+            assert_(len(result) == 2)
+            assert_array_equal(result[0], [1, 6])
+            assert_array_equal(result[1], [1, 2])
+            assert_equal(result[1].dtype.kind, 'i')
+
+
+def test_label01():
+    data = np.ones([])
+    out, n = ndimage.label(data)
+    assert_array_almost_equal(out, 1)
+    assert_equal(n, 1)
+
+
+def test_label02():
+    data = np.zeros([])
+    out, n = ndimage.label(data)
+    assert_array_almost_equal(out, 0)
+    assert_equal(n, 0)
+
+
+def test_label03():
+    data = np.ones([1])
+    out, n = ndimage.label(data)
+    assert_array_almost_equal(out, [1])
+    assert_equal(n, 1)
+
+
+def test_label04():
+    data = np.zeros([1])
+    out, n = ndimage.label(data)
+    assert_array_almost_equal(out, [0])
+    assert_equal(n, 0)
+
+
+def test_label05():
+    data = np.ones([5])
+    out, n = ndimage.label(data)
+    assert_array_almost_equal(out, [1, 1, 1, 1, 1])
+    assert_equal(n, 1)
+
+
+def test_label06():
+    data = np.array([1, 0, 1, 1, 0, 1])
+    out, n = ndimage.label(data)
+    assert_array_almost_equal(out, [1, 0, 2, 2, 0, 3])
+    assert_equal(n, 3)
+
+
+def test_label07():
+    data = np.array([[0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 0, 0, 0]])
+    out, n = ndimage.label(data)
+    assert_array_almost_equal(out, [[0, 0, 0, 0, 0, 0],
+                                    [0, 0, 0, 0, 0, 0],
+                                    [0, 0, 0, 0, 0, 0],
+                                    [0, 0, 0, 0, 0, 0],
+                                    [0, 0, 0, 0, 0, 0],
+                                    [0, 0, 0, 0, 0, 0]])
+    assert_equal(n, 0)
+
+
+def test_label08():
+    data = np.array([[1, 0, 0, 0, 0, 0],
+                     [0, 0, 1, 1, 0, 0],
+                     [0, 0, 1, 1, 1, 0],
+                     [1, 1, 0, 0, 0, 0],
+                     [1, 1, 0, 0, 0, 0],
+                     [0, 0, 0, 1, 1, 0]])
+    out, n = ndimage.label(data)
+    assert_array_almost_equal(out, [[1, 0, 0, 0, 0, 0],
+                                    [0, 0, 2, 2, 0, 0],
+                                    [0, 0, 2, 2, 2, 0],
+                                    [3, 3, 0, 0, 0, 0],
+                                    [3, 3, 0, 0, 0, 0],
+                                    [0, 0, 0, 4, 4, 0]])
+    assert_equal(n, 4)
+
+
+def test_label09():
+    data = np.array([[1, 0, 0, 0, 0, 0],
+                     [0, 0, 1, 1, 0, 0],
+                     [0, 0, 1, 1, 1, 0],
+                     [1, 1, 0, 0, 0, 0],
+                     [1, 1, 0, 0, 0, 0],
+                     [0, 0, 0, 1, 1, 0]])
+    struct = ndimage.generate_binary_structure(2, 2)
+    out, n = ndimage.label(data, struct)
+    assert_array_almost_equal(out, [[1, 0, 0, 0, 0, 0],
+                                    [0, 0, 2, 2, 0, 0],
+                                    [0, 0, 2, 2, 2, 0],
+                                    [2, 2, 0, 0, 0, 0],
+                                    [2, 2, 0, 0, 0, 0],
+                                    [0, 0, 0, 3, 3, 0]])
+    assert_equal(n, 3)
+
+
+def test_label10():
+    data = np.array([[0, 0, 0, 0, 0, 0],
+                     [0, 1, 1, 0, 1, 0],
+                     [0, 1, 1, 1, 1, 0],
+                     [0, 0, 0, 0, 0, 0]])
+    struct = ndimage.generate_binary_structure(2, 2)
+    out, n = ndimage.label(data, struct)
+    assert_array_almost_equal(out, [[0, 0, 0, 0, 0, 0],
+                                    [0, 1, 1, 0, 1, 0],
+                                    [0, 1, 1, 1, 1, 0],
+                                    [0, 0, 0, 0, 0, 0]])
+    assert_equal(n, 1)
+
+
+def test_label11():
+    for type in types:
+        data = np.array([[1, 0, 0, 0, 0, 0],
+                         [0, 0, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 0],
+                         [1, 1, 0, 0, 0, 0],
+                         [1, 1, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 0]], type)
+        out, n = ndimage.label(data)
+        expected = [[1, 0, 0, 0, 0, 0],
+                    [0, 0, 2, 2, 0, 0],
+                    [0, 0, 2, 2, 2, 0],
+                    [3, 3, 0, 0, 0, 0],
+                    [3, 3, 0, 0, 0, 0],
+                    [0, 0, 0, 4, 4, 0]]
+        assert_array_almost_equal(out, expected)
+        assert_equal(n, 4)
+
+
+def test_label11_inplace():
+    for type in types:
+        data = np.array([[1, 0, 0, 0, 0, 0],
+                         [0, 0, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 0],
+                         [1, 1, 0, 0, 0, 0],
+                         [1, 1, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 0]], type)
+        n = ndimage.label(data, output=data)
+        expected = [[1, 0, 0, 0, 0, 0],
+                    [0, 0, 2, 2, 0, 0],
+                    [0, 0, 2, 2, 2, 0],
+                    [3, 3, 0, 0, 0, 0],
+                    [3, 3, 0, 0, 0, 0],
+                    [0, 0, 0, 4, 4, 0]]
+        assert_array_almost_equal(data, expected)
+        assert_equal(n, 4)
+
+
+def test_label12():
+    for type in types:
+        data = np.array([[0, 0, 0, 0, 1, 1],
+                         [0, 0, 0, 0, 0, 1],
+                         [0, 0, 1, 0, 1, 1],
+                         [0, 0, 1, 1, 1, 1],
+                         [0, 0, 0, 1, 1, 0]], type)
+        out, n = ndimage.label(data)
+        expected = [[0, 0, 0, 0, 1, 1],
+                    [0, 0, 0, 0, 0, 1],
+                    [0, 0, 1, 0, 1, 1],
+                    [0, 0, 1, 1, 1, 1],
+                    [0, 0, 0, 1, 1, 0]]
+        assert_array_almost_equal(out, expected)
+        assert_equal(n, 1)
+
+
+def test_label13():
+    for type in types:
+        data = np.array([[1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1],
+                         [1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1],
+                         [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
+                         [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]],
+                        type)
+        out, n = ndimage.label(data)
+        expected = [[1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1],
+                    [1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1],
+                    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
+                    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
+        assert_array_almost_equal(out, expected)
+        assert_equal(n, 1)
+
+
+def test_label_output_typed():
+    data = np.ones([5])
+    for t in types:
+        output = np.zeros([5], dtype=t)
+        n = ndimage.label(data, output=output)
+        assert_array_almost_equal(output, 1)
+        assert_equal(n, 1)
+
+
+def test_label_output_dtype():
+    data = np.ones([5])
+    for t in types:
+        output, n = ndimage.label(data, output=t)
+        assert_array_almost_equal(output, 1)
+        assert output.dtype == t
+
+
+def test_label_output_wrong_size():
+    data = np.ones([5])
+    for t in types:
+        output = np.zeros([10], t)
+        assert_raises((RuntimeError, ValueError),
+                      ndimage.label, data, output=output)
+
+
+def test_label_structuring_elements():
+    data = np.loadtxt(os.path.join(os.path.dirname(
+        __file__), "data", "label_inputs.txt"))
+    strels = np.loadtxt(os.path.join(
+        os.path.dirname(__file__), "data", "label_strels.txt"))
+    results = np.loadtxt(os.path.join(
+        os.path.dirname(__file__), "data", "label_results.txt"))
+    data = data.reshape((-1, 7, 7))
+    strels = strels.reshape((-1, 3, 3))
+    results = results.reshape((-1, 7, 7))
+    r = 0
+    for i in range(data.shape[0]):
+        d = data[i, :, :]
+        for j in range(strels.shape[0]):
+            s = strels[j, :, :]
+            assert_equal(ndimage.label(d, s)[0], results[r, :, :])
+            r += 1
+
+
+def test_ticket_742():
+    def SE(img, thresh=.7, size=4):
+        mask = img > thresh
+        rank = len(mask.shape)
+        la, co = ndimage.label(mask,
+                               ndimage.generate_binary_structure(rank, rank))
+        _ = ndimage.find_objects(la)
+
+    if np.dtype(np.intp) != np.dtype('i'):
+        shape = (3, 1240, 1240)
+        a = np.random.rand(np.prod(shape)).reshape(shape)
+        # shouldn't crash
+        SE(a)
+
+
+def test_gh_issue_3025():
+    """Github issue #3025 - improper merging of labels"""
+    d = np.zeros((60, 320))
+    d[:, :257] = 1
+    d[:, 260:] = 1
+    d[36, 257] = 1
+    d[35, 258] = 1
+    d[35, 259] = 1
+    assert ndimage.label(d, np.ones((3, 3)))[1] == 1
+
+
+def test_label_default_dtype():
+    test_array = np.random.rand(10, 10)
+    label, no_features = ndimage.label(test_array > 0.5)
+    assert_(label.dtype in (np.int32, np.int64))
+    # Shouldn't raise an exception
+    ndimage.find_objects(label)
+
+
+def test_find_objects01():
+    data = np.ones([], dtype=int)
+    out = ndimage.find_objects(data)
+    assert_(out == [()])
+
+
+def test_find_objects02():
+    data = np.zeros([], dtype=int)
+    out = ndimage.find_objects(data)
+    assert_(out == [])
+
+
+def test_find_objects03():
+    data = np.ones([1], dtype=int)
+    out = ndimage.find_objects(data)
+    assert_equal(out, [(slice(0, 1, None),)])
+
+
+def test_find_objects04():
+    data = np.zeros([1], dtype=int)
+    out = ndimage.find_objects(data)
+    assert_equal(out, [])
+
+
+def test_find_objects05():
+    data = np.ones([5], dtype=int)
+    out = ndimage.find_objects(data)
+    assert_equal(out, [(slice(0, 5, None),)])
+
+
+def test_find_objects06():
+    data = np.array([1, 0, 2, 2, 0, 3])
+    out = ndimage.find_objects(data)
+    assert_equal(out, [(slice(0, 1, None),),
+                       (slice(2, 4, None),),
+                       (slice(5, 6, None),)])
+
+
+def test_find_objects07():
+    data = np.array([[0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 0, 0, 0]])
+    out = ndimage.find_objects(data)
+    assert_equal(out, [])
+
+
+def test_find_objects08():
+    data = np.array([[1, 0, 0, 0, 0, 0],
+                     [0, 0, 2, 2, 0, 0],
+                     [0, 0, 2, 2, 2, 0],
+                     [3, 3, 0, 0, 0, 0],
+                     [3, 3, 0, 0, 0, 0],
+                     [0, 0, 0, 4, 4, 0]])
+    out = ndimage.find_objects(data)
+    assert_equal(out, [(slice(0, 1, None), slice(0, 1, None)),
+                       (slice(1, 3, None), slice(2, 5, None)),
+                       (slice(3, 5, None), slice(0, 2, None)),
+                       (slice(5, 6, None), slice(3, 5, None))])
+
+
+def test_find_objects09():
+    data = np.array([[1, 0, 0, 0, 0, 0],
+                     [0, 0, 2, 2, 0, 0],
+                     [0, 0, 2, 2, 2, 0],
+                     [0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 4, 4, 0]])
+    out = ndimage.find_objects(data)
+    assert_equal(out, [(slice(0, 1, None), slice(0, 1, None)),
+                       (slice(1, 3, None), slice(2, 5, None)),
+                       None,
+                       (slice(5, 6, None), slice(3, 5, None))])
+
+
+def test_value_indices01():
+    "Test dictionary keys and entries"
+    data = np.array([[1, 0, 0, 0, 0, 0],
+                     [0, 0, 2, 2, 0, 0],
+                     [0, 0, 2, 2, 2, 0],
+                     [0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 0, 0, 0],
+                     [0, 0, 0, 4, 4, 0]])
+    vi = ndimage.value_indices(data, ignore_value=0)
+    true_keys = [1, 2, 4]
+    assert_equal(list(vi.keys()), true_keys)
+
+    truevi = {}
+    for k in true_keys:
+        truevi[k] = np.where(data == k)
+
+    vi = ndimage.value_indices(data, ignore_value=0)
+    assert_equal(vi, truevi)
+
+
+def test_value_indices02():
+    "Test input checking"
+    data = np.zeros((5, 4), dtype=np.float32)
+    msg = "Parameter 'arr' must be an integer array"
+    with assert_raises(ValueError, match=msg):
+        ndimage.value_indices(data)
+
+
+def test_value_indices03():
+    "Test different input array shapes, from 1-D to 4-D"
+    for shape in [(36,), (18, 2), (3, 3, 4), (3, 3, 2, 2)]:
+        a = np.array((12*[1]+12*[2]+12*[3]), dtype=np.int32).reshape(shape)
+        trueKeys = np.unique(a)
+        vi = ndimage.value_indices(a)
+        assert_equal(list(vi.keys()), list(trueKeys))
+        for k in trueKeys:
+            trueNdx = np.where(a == k)
+            assert_equal(vi[k], trueNdx)
+
+
+def test_sum01():
+    for type in types:
+        input = np.array([], type)
+        output = ndimage.sum(input)
+        assert_equal(output, 0.0)
+
+
+def test_sum02():
+    for type in types:
+        input = np.zeros([0, 4], type)
+        output = ndimage.sum(input)
+        assert_equal(output, 0.0)
+
+
+def test_sum03():
+    for type in types:
+        input = np.ones([], type)
+        output = ndimage.sum(input)
+        assert_almost_equal(output, 1.0)
+
+
+def test_sum04():
+    for type in types:
+        input = np.array([1, 2], type)
+        output = ndimage.sum(input)
+        assert_almost_equal(output, 3.0)
+
+
+def test_sum05():
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.sum(input)
+        assert_almost_equal(output, 10.0)
+
+
+def test_sum06():
+    labels = np.array([], bool)
+    for type in types:
+        input = np.array([], type)
+        output = ndimage.sum(input, labels=labels)
+        assert_equal(output, 0.0)
+
+
+def test_sum07():
+    labels = np.ones([0, 4], bool)
+    for type in types:
+        input = np.zeros([0, 4], type)
+        output = ndimage.sum(input, labels=labels)
+        assert_equal(output, 0.0)
+
+
+def test_sum08():
+    labels = np.array([1, 0], bool)
+    for type in types:
+        input = np.array([1, 2], type)
+        output = ndimage.sum(input, labels=labels)
+        assert_equal(output, 1.0)
+
+
+def test_sum09():
+    labels = np.array([1, 0], bool)
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.sum(input, labels=labels)
+        assert_almost_equal(output, 4.0)
+
+
+def test_sum10():
+    labels = np.array([1, 0], bool)
+    input = np.array([[1, 2], [3, 4]], bool)
+    output = ndimage.sum(input, labels=labels)
+    assert_almost_equal(output, 2.0)
+
+
+def test_sum11():
+    labels = np.array([1, 2], np.int8)
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.sum(input, labels=labels,
+                             index=2)
+        assert_almost_equal(output, 6.0)
+
+
+def test_sum12():
+    labels = np.array([[1, 2], [2, 4]], np.int8)
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.sum(input, labels=labels, index=[4, 8, 2])
+        assert_array_almost_equal(output, [4.0, 0.0, 5.0])
+
+
+def test_sum_labels():
+    labels = np.array([[1, 2], [2, 4]], np.int8)
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output_sum = ndimage.sum(input, labels=labels, index=[4, 8, 2])
+        output_labels = ndimage.sum_labels(
+            input, labels=labels, index=[4, 8, 2])
+
+        assert (output_sum == output_labels).all()
+        assert_array_almost_equal(output_labels, [4.0, 0.0, 5.0])
+
+
+def test_mean01():
+    labels = np.array([1, 0], bool)
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.mean(input, labels=labels)
+        assert_almost_equal(output, 2.0)
+
+
+def test_mean02():
+    labels = np.array([1, 0], bool)
+    input = np.array([[1, 2], [3, 4]], bool)
+    output = ndimage.mean(input, labels=labels)
+    assert_almost_equal(output, 1.0)
+
+
+def test_mean03():
+    labels = np.array([1, 2])
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.mean(input, labels=labels,
+                              index=2)
+        assert_almost_equal(output, 3.0)
+
+
+def test_mean04():
+    labels = np.array([[1, 2], [2, 4]], np.int8)
+    with np.errstate(all='ignore'):
+        for type in types:
+            input = np.array([[1, 2], [3, 4]], type)
+            output = ndimage.mean(input, labels=labels,
+                                  index=[4, 8, 2])
+            assert_array_almost_equal(output[[0, 2]], [4.0, 2.5])
+            assert_(np.isnan(output[1]))
+
+
+def test_minimum01():
+    labels = np.array([1, 0], bool)
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.minimum(input, labels=labels)
+        assert_almost_equal(output, 1.0)
+
+
+def test_minimum02():
+    labels = np.array([1, 0], bool)
+    input = np.array([[2, 2], [2, 4]], bool)
+    output = ndimage.minimum(input, labels=labels)
+    assert_almost_equal(output, 1.0)
+
+
+def test_minimum03():
+    labels = np.array([1, 2])
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.minimum(input, labels=labels,
+                                 index=2)
+        assert_almost_equal(output, 2.0)
+
+
+def test_minimum04():
+    labels = np.array([[1, 2], [2, 3]])
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.minimum(input, labels=labels,
+                                 index=[2, 3, 8])
+        assert_array_almost_equal(output, [2.0, 4.0, 0.0])
+
+
+def test_maximum01():
+    labels = np.array([1, 0], bool)
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.maximum(input, labels=labels)
+        assert_almost_equal(output, 3.0)
+
+
+def test_maximum02():
+    labels = np.array([1, 0], bool)
+    input = np.array([[2, 2], [2, 4]], bool)
+    output = ndimage.maximum(input, labels=labels)
+    assert_almost_equal(output, 1.0)
+
+
+def test_maximum03():
+    labels = np.array([1, 2])
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.maximum(input, labels=labels,
+                                 index=2)
+        assert_almost_equal(output, 4.0)
+
+
+def test_maximum04():
+    labels = np.array([[1, 2], [2, 3]])
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.maximum(input, labels=labels,
+                                 index=[2, 3, 8])
+        assert_array_almost_equal(output, [3.0, 4.0, 0.0])
+
+
+def test_maximum05():
+    # Regression test for ticket #501 (Trac)
+    x = np.array([-3, -2, -1])
+    assert_equal(ndimage.maximum(x), -1)
+
+
+def test_median01():
+    a = np.array([[1, 2, 0, 1],
+                  [5, 3, 0, 4],
+                  [0, 0, 0, 7],
+                  [9, 3, 0, 0]])
+    labels = np.array([[1, 1, 0, 2],
+                       [1, 1, 0, 2],
+                       [0, 0, 0, 2],
+                       [3, 3, 0, 0]])
+    output = ndimage.median(a, labels=labels, index=[1, 2, 3])
+    assert_array_almost_equal(output, [2.5, 4.0, 6.0])
+
+
+def test_median02():
+    a = np.array([[1, 2, 0, 1],
+                  [5, 3, 0, 4],
+                  [0, 0, 0, 7],
+                  [9, 3, 0, 0]])
+    output = ndimage.median(a)
+    assert_almost_equal(output, 1.0)
+
+
+def test_median03():
+    a = np.array([[1, 2, 0, 1],
+                  [5, 3, 0, 4],
+                  [0, 0, 0, 7],
+                  [9, 3, 0, 0]])
+    labels = np.array([[1, 1, 0, 2],
+                       [1, 1, 0, 2],
+                       [0, 0, 0, 2],
+                       [3, 3, 0, 0]])
+    output = ndimage.median(a, labels=labels)
+    assert_almost_equal(output, 3.0)
+
+
+def test_median_gh12836_bool():
+    # test boolean addition fix on example from gh-12836
+    a = np.asarray([1, 1], dtype=bool)
+    output = ndimage.median(a, labels=np.ones((2,)), index=[1])
+    assert_array_almost_equal(output, [1.0])
+
+
+def test_median_no_int_overflow():
+    # test integer overflow fix on example from gh-12836
+    a = np.asarray([65, 70], dtype=np.int8)
+    output = ndimage.median(a, labels=np.ones((2,)), index=[1])
+    assert_array_almost_equal(output, [67.5])
+
+
+def test_variance01():
+    with np.errstate(all='ignore'):
+        for type in types:
+            input = np.array([], type)
+            with suppress_warnings() as sup:
+                sup.filter(RuntimeWarning, "Mean of empty slice")
+                output = ndimage.variance(input)
+            assert_(np.isnan(output))
+
+
+def test_variance02():
+    for type in types:
+        input = np.array([1], type)
+        output = ndimage.variance(input)
+        assert_almost_equal(output, 0.0)
+
+
+def test_variance03():
+    for type in types:
+        input = np.array([1, 3], type)
+        output = ndimage.variance(input)
+        assert_almost_equal(output, 1.0)
+
+
+def test_variance04():
+    input = np.array([1, 0], bool)
+    output = ndimage.variance(input)
+    assert_almost_equal(output, 0.25)
+
+
+def test_variance05():
+    labels = [2, 2, 3]
+    for type in types:
+        input = np.array([1, 3, 8], type)
+        output = ndimage.variance(input, labels, 2)
+        assert_almost_equal(output, 1.0)
+
+
+def test_variance06():
+    labels = [2, 2, 3, 3, 4]
+    with np.errstate(all='ignore'):
+        for type in types:
+            input = np.array([1, 3, 8, 10, 8], type)
+            output = ndimage.variance(input, labels, [2, 3, 4])
+            assert_array_almost_equal(output, [1.0, 1.0, 0.0])
+
+
+def test_standard_deviation01():
+    with np.errstate(all='ignore'):
+        for type in types:
+            input = np.array([], type)
+            with suppress_warnings() as sup:
+                sup.filter(RuntimeWarning, "Mean of empty slice")
+                output = ndimage.standard_deviation(input)
+            assert_(np.isnan(output))
+
+
+def test_standard_deviation02():
+    for type in types:
+        input = np.array([1], type)
+        output = ndimage.standard_deviation(input)
+        assert_almost_equal(output, 0.0)
+
+
+def test_standard_deviation03():
+    for type in types:
+        input = np.array([1, 3], type)
+        output = ndimage.standard_deviation(input)
+        assert_almost_equal(output, np.sqrt(1.0))
+
+
+def test_standard_deviation04():
+    input = np.array([1, 0], bool)
+    output = ndimage.standard_deviation(input)
+    assert_almost_equal(output, 0.5)
+
+
+def test_standard_deviation05():
+    labels = [2, 2, 3]
+    for type in types:
+        input = np.array([1, 3, 8], type)
+        output = ndimage.standard_deviation(input, labels, 2)
+        assert_almost_equal(output, 1.0)
+
+
+def test_standard_deviation06():
+    labels = [2, 2, 3, 3, 4]
+    with np.errstate(all='ignore'):
+        for type in types:
+            input = np.array([1, 3, 8, 10, 8], type)
+            output = ndimage.standard_deviation(input, labels, [2, 3, 4])
+            assert_array_almost_equal(output, [1.0, 1.0, 0.0])
+
+
+def test_standard_deviation07():
+    labels = [1]
+    with np.errstate(all='ignore'):
+        for type in types:
+            input = np.array([-0.00619519], type)
+            output = ndimage.standard_deviation(input, labels, [1])
+            assert_array_almost_equal(output, [0])
+
+
+def test_minimum_position01():
+    labels = np.array([1, 0], bool)
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.minimum_position(input, labels=labels)
+        assert_equal(output, (0, 0))
+
+
+def test_minimum_position02():
+    for type in types:
+        input = np.array([[5, 4, 2, 5],
+                          [3, 7, 0, 2],
+                          [1, 5, 1, 1]], type)
+        output = ndimage.minimum_position(input)
+        assert_equal(output, (1, 2))
+
+
+def test_minimum_position03():
+    input = np.array([[5, 4, 2, 5],
+                      [3, 7, 0, 2],
+                      [1, 5, 1, 1]], bool)
+    output = ndimage.minimum_position(input)
+    assert_equal(output, (1, 2))
+
+
+def test_minimum_position04():
+    input = np.array([[5, 4, 2, 5],
+                      [3, 7, 1, 2],
+                      [1, 5, 1, 1]], bool)
+    output = ndimage.minimum_position(input)
+    assert_equal(output, (0, 0))
+
+
+def test_minimum_position05():
+    labels = [1, 2, 0, 4]
+    for type in types:
+        input = np.array([[5, 4, 2, 5],
+                          [3, 7, 0, 2],
+                          [1, 5, 2, 3]], type)
+        output = ndimage.minimum_position(input, labels)
+        assert_equal(output, (2, 0))
+
+
+def test_minimum_position06():
+    labels = [1, 2, 3, 4]
+    for type in types:
+        input = np.array([[5, 4, 2, 5],
+                          [3, 7, 0, 2],
+                          [1, 5, 1, 1]], type)
+        output = ndimage.minimum_position(input, labels, 2)
+        assert_equal(output, (0, 1))
+
+
+def test_minimum_position07():
+    labels = [1, 2, 3, 4]
+    for type in types:
+        input = np.array([[5, 4, 2, 5],
+                          [3, 7, 0, 2],
+                          [1, 5, 1, 1]], type)
+        output = ndimage.minimum_position(input, labels,
+                                          [2, 3])
+        assert_equal(output[0], (0, 1))
+        assert_equal(output[1], (1, 2))
+
+
+def test_maximum_position01():
+    labels = np.array([1, 0], bool)
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output = ndimage.maximum_position(input,
+                                          labels=labels)
+        assert_equal(output, (1, 0))
+
+
+def test_maximum_position02():
+    for type in types:
+        input = np.array([[5, 4, 2, 5],
+                          [3, 7, 8, 2],
+                          [1, 5, 1, 1]], type)
+        output = ndimage.maximum_position(input)
+        assert_equal(output, (1, 2))
+
+
+def test_maximum_position03():
+    input = np.array([[5, 4, 2, 5],
+                      [3, 7, 8, 2],
+                      [1, 5, 1, 1]], bool)
+    output = ndimage.maximum_position(input)
+    assert_equal(output, (0, 0))
+
+
+def test_maximum_position04():
+    labels = [1, 2, 0, 4]
+    for type in types:
+        input = np.array([[5, 4, 2, 5],
+                          [3, 7, 8, 2],
+                          [1, 5, 1, 1]], type)
+        output = ndimage.maximum_position(input, labels)
+        assert_equal(output, (1, 1))
+
+
+def test_maximum_position05():
+    labels = [1, 2, 0, 4]
+    for type in types:
+        input = np.array([[5, 4, 2, 5],
+                          [3, 7, 8, 2],
+                          [1, 5, 1, 1]], type)
+        output = ndimage.maximum_position(input, labels, 1)
+        assert_equal(output, (0, 0))
+
+
+def test_maximum_position06():
+    labels = [1, 2, 0, 4]
+    for type in types:
+        input = np.array([[5, 4, 2, 5],
+                          [3, 7, 8, 2],
+                          [1, 5, 1, 1]], type)
+        output = ndimage.maximum_position(input, labels,
+                                          [1, 2])
+        assert_equal(output[0], (0, 0))
+        assert_equal(output[1], (1, 1))
+
+
+def test_maximum_position07():
+    # Test float labels
+    labels = np.array([1.0, 2.5, 0.0, 4.5])
+    for type in types:
+        input = np.array([[5, 4, 2, 5],
+                          [3, 7, 8, 2],
+                          [1, 5, 1, 1]], type)
+        output = ndimage.maximum_position(input, labels,
+                                          [1.0, 4.5])
+        assert_equal(output[0], (0, 0))
+        assert_equal(output[1], (0, 3))
+
+
+def test_extrema01():
+    labels = np.array([1, 0], bool)
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output1 = ndimage.extrema(input, labels=labels)
+        output2 = ndimage.minimum(input, labels=labels)
+        output3 = ndimage.maximum(input, labels=labels)
+        output4 = ndimage.minimum_position(input,
+                                           labels=labels)
+        output5 = ndimage.maximum_position(input,
+                                           labels=labels)
+        assert_equal(output1, (output2, output3, output4, output5))
+
+
+def test_extrema02():
+    labels = np.array([1, 2])
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output1 = ndimage.extrema(input, labels=labels,
+                                  index=2)
+        output2 = ndimage.minimum(input, labels=labels,
+                                  index=2)
+        output3 = ndimage.maximum(input, labels=labels,
+                                  index=2)
+        output4 = ndimage.minimum_position(input,
+                                           labels=labels, index=2)
+        output5 = ndimage.maximum_position(input,
+                                           labels=labels, index=2)
+        assert_equal(output1, (output2, output3, output4, output5))
+
+
+def test_extrema03():
+    labels = np.array([[1, 2], [2, 3]])
+    for type in types:
+        input = np.array([[1, 2], [3, 4]], type)
+        output1 = ndimage.extrema(input, labels=labels,
+                                  index=[2, 3, 8])
+        output2 = ndimage.minimum(input, labels=labels,
+                                  index=[2, 3, 8])
+        output3 = ndimage.maximum(input, labels=labels,
+                                  index=[2, 3, 8])
+        output4 = ndimage.minimum_position(input,
+                                           labels=labels, index=[2, 3, 8])
+        output5 = ndimage.maximum_position(input,
+                                           labels=labels, index=[2, 3, 8])
+        assert_array_almost_equal(output1[0], output2)
+        assert_array_almost_equal(output1[1], output3)
+        assert_array_almost_equal(output1[2], output4)
+        assert_array_almost_equal(output1[3], output5)
+
+
+def test_extrema04():
+    labels = [1, 2, 0, 4]
+    for type in types:
+        input = np.array([[5, 4, 2, 5],
+                          [3, 7, 8, 2],
+                          [1, 5, 1, 1]], type)
+        output1 = ndimage.extrema(input, labels, [1, 2])
+        output2 = ndimage.minimum(input, labels, [1, 2])
+        output3 = ndimage.maximum(input, labels, [1, 2])
+        output4 = ndimage.minimum_position(input, labels,
+                                           [1, 2])
+        output5 = ndimage.maximum_position(input, labels,
+                                           [1, 2])
+        assert_array_almost_equal(output1[0], output2)
+        assert_array_almost_equal(output1[1], output3)
+        assert_array_almost_equal(output1[2], output4)
+        assert_array_almost_equal(output1[3], output5)
+
+
+def test_center_of_mass01():
+    expected = [0.0, 0.0]
+    for type in types:
+        input = np.array([[1, 0], [0, 0]], type)
+        output = ndimage.center_of_mass(input)
+        assert_array_almost_equal(output, expected)
+
+
+def test_center_of_mass02():
+    expected = [1, 0]
+    for type in types:
+        input = np.array([[0, 0], [1, 0]], type)
+        output = ndimage.center_of_mass(input)
+        assert_array_almost_equal(output, expected)
+
+
+def test_center_of_mass03():
+    expected = [0, 1]
+    for type in types:
+        input = np.array([[0, 1], [0, 0]], type)
+        output = ndimage.center_of_mass(input)
+        assert_array_almost_equal(output, expected)
+
+
+def test_center_of_mass04():
+    expected = [1, 1]
+    for type in types:
+        input = np.array([[0, 0], [0, 1]], type)
+        output = ndimage.center_of_mass(input)
+        assert_array_almost_equal(output, expected)
+
+
+def test_center_of_mass05():
+    expected = [0.5, 0.5]
+    for type in types:
+        input = np.array([[1, 1], [1, 1]], type)
+        output = ndimage.center_of_mass(input)
+        assert_array_almost_equal(output, expected)
+
+
+def test_center_of_mass06():
+    expected = [0.5, 0.5]
+    input = np.array([[1, 2], [3, 1]], bool)
+    output = ndimage.center_of_mass(input)
+    assert_array_almost_equal(output, expected)
+
+
+def test_center_of_mass07():
+    labels = [1, 0]
+    expected = [0.5, 0.0]
+    input = np.array([[1, 2], [3, 1]], bool)
+    output = ndimage.center_of_mass(input, labels)
+    assert_array_almost_equal(output, expected)
+
+
+def test_center_of_mass08():
+    labels = [1, 2]
+    expected = [0.5, 1.0]
+    input = np.array([[5, 2], [3, 1]], bool)
+    output = ndimage.center_of_mass(input, labels, 2)
+    assert_array_almost_equal(output, expected)
+
+
+def test_center_of_mass09():
+    labels = [1, 2]
+    expected = [(0.5, 0.0), (0.5, 1.0)]
+    input = np.array([[1, 2], [1, 1]], bool)
+    output = ndimage.center_of_mass(input, labels, [1, 2])
+    assert_array_almost_equal(output, expected)
+
+
+def test_histogram01():
+    expected = np.ones(10)
+    input = np.arange(10)
+    output = ndimage.histogram(input, 0, 10, 10)
+    assert_array_almost_equal(output, expected)
+
+
+def test_histogram02():
+    labels = [1, 1, 1, 1, 2, 2, 2, 2]
+    expected = [0, 2, 0, 1, 1]
+    input = np.array([1, 1, 3, 4, 3, 3, 3, 3])
+    output = ndimage.histogram(input, 0, 4, 5, labels, 1)
+    assert_array_almost_equal(output, expected)
+
+
+def test_histogram03():
+    labels = [1, 0, 1, 1, 2, 2, 2, 2]
+    expected1 = [0, 1, 0, 1, 1]
+    expected2 = [0, 0, 0, 3, 0]
+    input = np.array([1, 1, 3, 4, 3, 5, 3, 3])
+    output = ndimage.histogram(input, 0, 4, 5, labels, (1, 2))
+
+    assert_array_almost_equal(output[0], expected1)
+    assert_array_almost_equal(output[1], expected2)
+
+
+def test_stat_funcs_2d():
+    a = np.array([[5, 6, 0, 0, 0], [8, 9, 0, 0, 0], [0, 0, 0, 3, 5]])
+    lbl = np.array([[1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [0, 0, 0, 2, 2]])
+
+    mean = ndimage.mean(a, labels=lbl, index=[1, 2])
+    assert_array_equal(mean, [7.0, 4.0])
+
+    var = ndimage.variance(a, labels=lbl, index=[1, 2])
+    assert_array_equal(var, [2.5, 1.0])
+
+    std = ndimage.standard_deviation(a, labels=lbl, index=[1, 2])
+    assert_array_almost_equal(std, np.sqrt([2.5, 1.0]))
+
+    med = ndimage.median(a, labels=lbl, index=[1, 2])
+    assert_array_equal(med, [7.0, 4.0])
+
+    min = ndimage.minimum(a, labels=lbl, index=[1, 2])
+    assert_array_equal(min, [5, 3])
+
+    max = ndimage.maximum(a, labels=lbl, index=[1, 2])
+    assert_array_equal(max, [9, 5])
+
+
+class TestWatershedIft:
+
+    def test_watershed_ift01(self):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 0, 0, 0, 1, 0],
+                         [0, 1, 0, 0, 0, 1, 0],
+                         [0, 1, 0, 0, 0, 1, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0]], np.uint8)
+        markers = np.array([[-1, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 1, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0]], np.int8)
+        out = ndimage.watershed_ift(data, markers, structure=[[1, 1, 1],
+                                                              [1, 1, 1],
+                                                              [1, 1, 1]])
+        expected = [[-1, -1, -1, -1, -1, -1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, -1, -1, -1, -1, -1, -1],
+                    [-1, -1, -1, -1, -1, -1, -1]]
+        assert_array_almost_equal(out, expected)
+
+    def test_watershed_ift02(self):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 0, 0, 0, 1, 0],
+                         [0, 1, 0, 0, 0, 1, 0],
+                         [0, 1, 0, 0, 0, 1, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0]], np.uint8)
+        markers = np.array([[-1, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 1, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0]], np.int8)
+        out = ndimage.watershed_ift(data, markers)
+        expected = [[-1, -1, -1, -1, -1, -1, -1],
+                    [-1, -1, 1, 1, 1, -1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, -1, 1, 1, 1, -1, -1],
+                    [-1, -1, -1, -1, -1, -1, -1],
+                    [-1, -1, -1, -1, -1, -1, -1]]
+        assert_array_almost_equal(out, expected)
+
+    def test_watershed_ift03(self):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 0, 1, 0, 1, 0],
+                         [0, 1, 0, 1, 0, 1, 0],
+                         [0, 1, 0, 1, 0, 1, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0]], np.uint8)
+        markers = np.array([[0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 2, 0, 3, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, -1]], np.int8)
+        out = ndimage.watershed_ift(data, markers)
+        expected = [[-1, -1, -1, -1, -1, -1, -1],
+                    [-1, -1, 2, -1, 3, -1, -1],
+                    [-1, 2, 2, 3, 3, 3, -1],
+                    [-1, 2, 2, 3, 3, 3, -1],
+                    [-1, 2, 2, 3, 3, 3, -1],
+                    [-1, -1, 2, -1, 3, -1, -1],
+                    [-1, -1, -1, -1, -1, -1, -1]]
+        assert_array_almost_equal(out, expected)
+
+    def test_watershed_ift04(self):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 0, 1, 0, 1, 0],
+                         [0, 1, 0, 1, 0, 1, 0],
+                         [0, 1, 0, 1, 0, 1, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0]], np.uint8)
+        markers = np.array([[0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 2, 0, 3, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, -1]],
+                           np.int8)
+        out = ndimage.watershed_ift(data, markers,
+                                    structure=[[1, 1, 1],
+                                               [1, 1, 1],
+                                               [1, 1, 1]])
+        expected = [[-1, -1, -1, -1, -1, -1, -1],
+                    [-1, 2, 2, 3, 3, 3, -1],
+                    [-1, 2, 2, 3, 3, 3, -1],
+                    [-1, 2, 2, 3, 3, 3, -1],
+                    [-1, 2, 2, 3, 3, 3, -1],
+                    [-1, 2, 2, 3, 3, 3, -1],
+                    [-1, -1, -1, -1, -1, -1, -1]]
+        assert_array_almost_equal(out, expected)
+
+    def test_watershed_ift05(self):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 0, 1, 0, 1, 0],
+                         [0, 1, 0, 1, 0, 1, 0],
+                         [0, 1, 0, 1, 0, 1, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0]], np.uint8)
+        markers = np.array([[0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 3, 0, 2, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, -1]],
+                           np.int8)
+        out = ndimage.watershed_ift(data, markers,
+                                    structure=[[1, 1, 1],
+                                               [1, 1, 1],
+                                               [1, 1, 1]])
+        expected = [[-1, -1, -1, -1, -1, -1, -1],
+                    [-1, 3, 3, 2, 2, 2, -1],
+                    [-1, 3, 3, 2, 2, 2, -1],
+                    [-1, 3, 3, 2, 2, 2, -1],
+                    [-1, 3, 3, 2, 2, 2, -1],
+                    [-1, 3, 3, 2, 2, 2, -1],
+                    [-1, -1, -1, -1, -1, -1, -1]]
+        assert_array_almost_equal(out, expected)
+
+    def test_watershed_ift06(self):
+        data = np.array([[0, 1, 0, 0, 0, 1, 0],
+                         [0, 1, 0, 0, 0, 1, 0],
+                         [0, 1, 0, 0, 0, 1, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0]], np.uint8)
+        markers = np.array([[-1, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 1, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0]], np.int8)
+        out = ndimage.watershed_ift(data, markers,
+                                    structure=[[1, 1, 1],
+                                               [1, 1, 1],
+                                               [1, 1, 1]])
+        expected = [[-1, 1, 1, 1, 1, 1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, -1, -1, -1, -1, -1, -1],
+                    [-1, -1, -1, -1, -1, -1, -1]]
+        assert_array_almost_equal(out, expected)
+
+    def test_watershed_ift07(self):
+        shape = (7, 6)
+        data = np.zeros(shape, dtype=np.uint8)
+        data = data.transpose()
+        data[...] = np.array([[0, 1, 0, 0, 0, 1, 0],
+                              [0, 1, 0, 0, 0, 1, 0],
+                              [0, 1, 0, 0, 0, 1, 0],
+                              [0, 1, 1, 1, 1, 1, 0],
+                              [0, 0, 0, 0, 0, 0, 0],
+                              [0, 0, 0, 0, 0, 0, 0]], np.uint8)
+        markers = np.array([[-1, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 1, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0]], np.int8)
+        out = np.zeros(shape, dtype=np.int16)
+        out = out.transpose()
+        ndimage.watershed_ift(data, markers,
+                              structure=[[1, 1, 1],
+                                         [1, 1, 1],
+                                         [1, 1, 1]],
+                              output=out)
+        expected = [[-1, 1, 1, 1, 1, 1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, 1, 1, 1, 1, 1, -1],
+                    [-1, -1, -1, -1, -1, -1, -1],
+                    [-1, -1, -1, -1, -1, -1, -1]]
+        assert_array_almost_equal(out, expected)
+
+    def test_watershed_ift08(self):
+        # Test cost larger than uint8. See gh-10069.
+        data = np.array([[256, 0],
+                         [0, 0]], np.uint16)
+        markers = np.array([[1, 0],
+                            [0, 0]], np.int8)
+        out = ndimage.watershed_ift(data, markers)
+        expected = [[1, 1],
+                    [1, 1]]
+        assert_array_almost_equal(out, expected)
+
+    def test_watershed_ift09(self):
+        # Test large cost. See gh-19575
+        data = np.array([[np.iinfo(np.uint16).max, 0],
+                         [0, 0]], np.uint16)
+        markers = np.array([[1, 0],
+                            [0, 0]], np.int8)
+        out = ndimage.watershed_ift(data, markers)
+        expected = [[1, 1],
+                    [1, 1]]
+        assert_allclose(out, expected)
+
+
+@pytest.mark.parametrize("dt", [np.intc, np.uintc])
+def test_gh_19423(dt):
+    rng = np.random.default_rng(123)
+    max_val = 8
+    image = rng.integers(low=0, high=max_val, size=(10, 12)).astype(dtype=dt)
+    val_idx = ndimage.value_indices(image)
+    assert len(val_idx.keys()) == max_val
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_morphology.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_morphology.py
new file mode 100644
index 0000000000000000000000000000000000000000..29094d430edb282999108994297b6fd79003408f
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_morphology.py
@@ -0,0 +1,2394 @@
+import numpy as np
+from numpy.testing import (assert_, assert_equal, assert_array_equal,
+                           assert_array_almost_equal)
+import pytest
+from pytest import raises as assert_raises
+
+from scipy import ndimage
+
+from . import types
+
+
+class TestNdimageMorphology:
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_bf01(self, dtype):
+        # brute force (bf) distance transform
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out, ft = ndimage.distance_transform_bf(data, 'euclidean',
+                                                return_indices=True)
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                    [0, 0, 1, 2, 4, 2, 1, 0, 0],
+                    [0, 0, 1, 4, 8, 4, 1, 0, 0],
+                    [0, 0, 1, 2, 4, 2, 1, 0, 0],
+                    [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0]]
+        assert_array_almost_equal(out * out, expected)
+
+        expected = [[[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                     [1, 1, 1, 1, 1, 1, 1, 1, 1],
+                     [2, 2, 2, 2, 1, 2, 2, 2, 2],
+                     [3, 3, 3, 2, 1, 2, 3, 3, 3],
+                     [4, 4, 4, 4, 6, 4, 4, 4, 4],
+                     [5, 5, 6, 6, 7, 6, 6, 5, 5],
+                     [6, 6, 6, 7, 7, 7, 6, 6, 6],
+                     [7, 7, 7, 7, 7, 7, 7, 7, 7],
+                     [8, 8, 8, 8, 8, 8, 8, 8, 8]],
+                    [[0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 2, 4, 6, 6, 7, 8],
+                     [0, 1, 1, 2, 4, 6, 7, 7, 8],
+                     [0, 1, 1, 1, 6, 7, 7, 7, 8],
+                     [0, 1, 2, 2, 4, 6, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8]]]
+        assert_array_almost_equal(ft, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_bf02(self, dtype):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out, ft = ndimage.distance_transform_bf(data, 'cityblock',
+                                                return_indices=True)
+
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                    [0, 0, 1, 2, 2, 2, 1, 0, 0],
+                    [0, 0, 1, 2, 3, 2, 1, 0, 0],
+                    [0, 0, 1, 2, 2, 2, 1, 0, 0],
+                    [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0]]
+        assert_array_almost_equal(out, expected)
+
+        expected = [[[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                     [1, 1, 1, 1, 1, 1, 1, 1, 1],
+                     [2, 2, 2, 2, 1, 2, 2, 2, 2],
+                     [3, 3, 3, 3, 1, 3, 3, 3, 3],
+                     [4, 4, 4, 4, 7, 4, 4, 4, 4],
+                     [5, 5, 6, 7, 7, 7, 6, 5, 5],
+                     [6, 6, 6, 7, 7, 7, 6, 6, 6],
+                     [7, 7, 7, 7, 7, 7, 7, 7, 7],
+                     [8, 8, 8, 8, 8, 8, 8, 8, 8]],
+                    [[0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 2, 4, 6, 6, 7, 8],
+                     [0, 1, 1, 1, 4, 7, 7, 7, 8],
+                     [0, 1, 1, 1, 4, 7, 7, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8]]]
+        assert_array_almost_equal(expected, ft)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_bf03(self, dtype):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out, ft = ndimage.distance_transform_bf(data, 'chessboard',
+                                                return_indices=True)
+
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                    [0, 0, 1, 1, 2, 1, 1, 0, 0],
+                    [0, 0, 1, 2, 2, 2, 1, 0, 0],
+                    [0, 0, 1, 1, 2, 1, 1, 0, 0],
+                    [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0]]
+        assert_array_almost_equal(out, expected)
+
+        expected = [[[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                     [1, 1, 1, 1, 1, 1, 1, 1, 1],
+                     [2, 2, 2, 2, 1, 2, 2, 2, 2],
+                     [3, 3, 4, 2, 2, 2, 4, 3, 3],
+                     [4, 4, 5, 6, 6, 6, 5, 4, 4],
+                     [5, 5, 6, 6, 7, 6, 6, 5, 5],
+                     [6, 6, 6, 7, 7, 7, 6, 6, 6],
+                     [7, 7, 7, 7, 7, 7, 7, 7, 7],
+                     [8, 8, 8, 8, 8, 8, 8, 8, 8]],
+                    [[0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 2, 5, 6, 6, 7, 8],
+                     [0, 1, 1, 2, 6, 6, 7, 7, 8],
+                     [0, 1, 1, 2, 6, 7, 7, 7, 8],
+                     [0, 1, 2, 2, 6, 6, 7, 7, 8],
+                     [0, 1, 2, 4, 5, 6, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8]]]
+        assert_array_almost_equal(ft, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_bf04(self, dtype):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        tdt, tft = ndimage.distance_transform_bf(data, return_indices=1)
+        dts = []
+        fts = []
+        dt = np.zeros(data.shape, dtype=np.float64)
+        ndimage.distance_transform_bf(data, distances=dt)
+        dts.append(dt)
+        ft = ndimage.distance_transform_bf(
+            data, return_distances=False, return_indices=1)
+        fts.append(ft)
+        ft = np.indices(data.shape, dtype=np.int32)
+        ndimage.distance_transform_bf(
+            data, return_distances=False, return_indices=True, indices=ft)
+        fts.append(ft)
+        dt, ft = ndimage.distance_transform_bf(
+            data, return_indices=1)
+        dts.append(dt)
+        fts.append(ft)
+        dt = np.zeros(data.shape, dtype=np.float64)
+        ft = ndimage.distance_transform_bf(
+            data, distances=dt, return_indices=True)
+        dts.append(dt)
+        fts.append(ft)
+        ft = np.indices(data.shape, dtype=np.int32)
+        dt = ndimage.distance_transform_bf(
+            data, return_indices=True, indices=ft)
+        dts.append(dt)
+        fts.append(ft)
+        dt = np.zeros(data.shape, dtype=np.float64)
+        ft = np.indices(data.shape, dtype=np.int32)
+        ndimage.distance_transform_bf(
+            data, distances=dt, return_indices=True, indices=ft)
+        dts.append(dt)
+        fts.append(ft)
+        for dt in dts:
+            assert_array_almost_equal(tdt, dt)
+        for ft in fts:
+            assert_array_almost_equal(tft, ft)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_bf05(self, dtype):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out, ft = ndimage.distance_transform_bf(
+            data, 'euclidean', return_indices=True, sampling=[2, 2])
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 4, 4, 4, 0, 0, 0],
+                    [0, 0, 4, 8, 16, 8, 4, 0, 0],
+                    [0, 0, 4, 16, 32, 16, 4, 0, 0],
+                    [0, 0, 4, 8, 16, 8, 4, 0, 0],
+                    [0, 0, 0, 4, 4, 4, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0]]
+        assert_array_almost_equal(out * out, expected)
+
+        expected = [[[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                     [1, 1, 1, 1, 1, 1, 1, 1, 1],
+                     [2, 2, 2, 2, 1, 2, 2, 2, 2],
+                     [3, 3, 3, 2, 1, 2, 3, 3, 3],
+                     [4, 4, 4, 4, 6, 4, 4, 4, 4],
+                     [5, 5, 6, 6, 7, 6, 6, 5, 5],
+                     [6, 6, 6, 7, 7, 7, 6, 6, 6],
+                     [7, 7, 7, 7, 7, 7, 7, 7, 7],
+                     [8, 8, 8, 8, 8, 8, 8, 8, 8]],
+                    [[0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 2, 4, 6, 6, 7, 8],
+                     [0, 1, 1, 2, 4, 6, 7, 7, 8],
+                     [0, 1, 1, 1, 6, 7, 7, 7, 8],
+                     [0, 1, 2, 2, 4, 6, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8]]]
+        assert_array_almost_equal(ft, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_bf06(self, dtype):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out, ft = ndimage.distance_transform_bf(
+            data, 'euclidean', return_indices=True, sampling=[2, 1])
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 4, 1, 0, 0, 0],
+                    [0, 0, 1, 4, 8, 4, 1, 0, 0],
+                    [0, 0, 1, 4, 9, 4, 1, 0, 0],
+                    [0, 0, 1, 4, 8, 4, 1, 0, 0],
+                    [0, 0, 0, 1, 4, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0, 0]]
+        assert_array_almost_equal(out * out, expected)
+
+        expected = [[[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                     [1, 1, 1, 1, 1, 1, 1, 1, 1],
+                     [2, 2, 2, 2, 2, 2, 2, 2, 2],
+                     [3, 3, 3, 3, 2, 3, 3, 3, 3],
+                     [4, 4, 4, 4, 4, 4, 4, 4, 4],
+                     [5, 5, 5, 5, 6, 5, 5, 5, 5],
+                     [6, 6, 6, 6, 7, 6, 6, 6, 6],
+                     [7, 7, 7, 7, 7, 7, 7, 7, 7],
+                     [8, 8, 8, 8, 8, 8, 8, 8, 8]],
+                    [[0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 2, 6, 6, 6, 7, 8],
+                     [0, 1, 1, 1, 6, 7, 7, 7, 8],
+                     [0, 1, 1, 1, 7, 7, 7, 7, 8],
+                     [0, 1, 1, 1, 6, 7, 7, 7, 8],
+                     [0, 1, 2, 2, 4, 6, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8]]]
+        assert_array_almost_equal(ft, expected)
+
+    def test_distance_transform_bf07(self):
+        # test input validation per discussion on PR #13302
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]])
+        with assert_raises(RuntimeError):
+            ndimage.distance_transform_bf(
+                data, return_distances=False, return_indices=False
+            )
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_cdt01(self, dtype):
+        # chamfer type distance (cdt) transform
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out, ft = ndimage.distance_transform_cdt(
+            data, 'cityblock', return_indices=True)
+        bf = ndimage.distance_transform_bf(data, 'cityblock')
+        assert_array_almost_equal(bf, out)
+
+        expected = [[[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                     [1, 1, 1, 1, 1, 1, 1, 1, 1],
+                     [2, 2, 2, 1, 1, 1, 2, 2, 2],
+                     [3, 3, 2, 1, 1, 1, 2, 3, 3],
+                     [4, 4, 4, 4, 1, 4, 4, 4, 4],
+                     [5, 5, 5, 5, 7, 7, 6, 5, 5],
+                     [6, 6, 6, 6, 7, 7, 6, 6, 6],
+                     [7, 7, 7, 7, 7, 7, 7, 7, 7],
+                     [8, 8, 8, 8, 8, 8, 8, 8, 8]],
+                    [[0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 1, 1, 4, 7, 7, 7, 8],
+                     [0, 1, 1, 1, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 2, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8]]]
+        assert_array_almost_equal(ft, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_cdt02(self, dtype):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out, ft = ndimage.distance_transform_cdt(data, 'chessboard',
+                                                 return_indices=True)
+        bf = ndimage.distance_transform_bf(data, 'chessboard')
+        assert_array_almost_equal(bf, out)
+
+        expected = [[[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                     [1, 1, 1, 1, 1, 1, 1, 1, 1],
+                     [2, 2, 2, 1, 1, 1, 2, 2, 2],
+                     [3, 3, 2, 2, 1, 2, 2, 3, 3],
+                     [4, 4, 3, 2, 2, 2, 3, 4, 4],
+                     [5, 5, 4, 6, 7, 6, 4, 5, 5],
+                     [6, 6, 6, 6, 7, 7, 6, 6, 6],
+                     [7, 7, 7, 7, 7, 7, 7, 7, 7],
+                     [8, 8, 8, 8, 8, 8, 8, 8, 8]],
+                    [[0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 2, 3, 4, 6, 7, 8],
+                     [0, 1, 1, 2, 2, 6, 6, 7, 8],
+                     [0, 1, 1, 1, 2, 6, 7, 7, 8],
+                     [0, 1, 1, 2, 6, 6, 7, 7, 8],
+                     [0, 1, 2, 2, 5, 6, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8],
+                     [0, 1, 2, 3, 4, 5, 6, 7, 8]]]
+        assert_array_almost_equal(ft, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_cdt03(self, dtype):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        tdt, tft = ndimage.distance_transform_cdt(data, return_indices=True)
+        dts = []
+        fts = []
+        dt = np.zeros(data.shape, dtype=np.int32)
+        ndimage.distance_transform_cdt(data, distances=dt)
+        dts.append(dt)
+        ft = ndimage.distance_transform_cdt(
+            data, return_distances=False, return_indices=True)
+        fts.append(ft)
+        ft = np.indices(data.shape, dtype=np.int32)
+        ndimage.distance_transform_cdt(
+            data, return_distances=False, return_indices=True, indices=ft)
+        fts.append(ft)
+        dt, ft = ndimage.distance_transform_cdt(
+            data, return_indices=True)
+        dts.append(dt)
+        fts.append(ft)
+        dt = np.zeros(data.shape, dtype=np.int32)
+        ft = ndimage.distance_transform_cdt(
+            data, distances=dt, return_indices=True)
+        dts.append(dt)
+        fts.append(ft)
+        ft = np.indices(data.shape, dtype=np.int32)
+        dt = ndimage.distance_transform_cdt(
+            data, return_indices=True, indices=ft)
+        dts.append(dt)
+        fts.append(ft)
+        dt = np.zeros(data.shape, dtype=np.int32)
+        ft = np.indices(data.shape, dtype=np.int32)
+        ndimage.distance_transform_cdt(data, distances=dt,
+                                       return_indices=True, indices=ft)
+        dts.append(dt)
+        fts.append(ft)
+        for dt in dts:
+            assert_array_almost_equal(tdt, dt)
+        for ft in fts:
+            assert_array_almost_equal(tft, ft)
+
+    def test_distance_transform_cdt04(self):
+        # test input validation per discussion on PR #13302
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]])
+        indices_out = np.zeros((data.ndim,) + data.shape, dtype=np.int32)
+        with assert_raises(RuntimeError):
+            ndimage.distance_transform_bf(
+                data,
+                return_distances=True,
+                return_indices=False,
+                indices=indices_out
+            )
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_cdt05(self, dtype):
+        # test custom metric type per discussion on issue #17381
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        metric_arg = np.ones((3, 3))
+        actual = ndimage.distance_transform_cdt(data, metric=metric_arg)
+        assert actual.sum() == -21
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_edt01(self, dtype):
+        # euclidean distance transform (edt)
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out, ft = ndimage.distance_transform_edt(data, return_indices=True)
+        bf = ndimage.distance_transform_bf(data, 'euclidean')
+        assert_array_almost_equal(bf, out)
+
+        dt = ft - np.indices(ft.shape[1:], dtype=ft.dtype)
+        dt = dt.astype(np.float64)
+        np.multiply(dt, dt, dt)
+        dt = np.add.reduce(dt, axis=0)
+        np.sqrt(dt, dt)
+
+        assert_array_almost_equal(bf, dt)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_edt02(self, dtype):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        tdt, tft = ndimage.distance_transform_edt(data, return_indices=True)
+        dts = []
+        fts = []
+        dt = np.zeros(data.shape, dtype=np.float64)
+        ndimage.distance_transform_edt(data, distances=dt)
+        dts.append(dt)
+        ft = ndimage.distance_transform_edt(
+            data, return_distances=0, return_indices=True)
+        fts.append(ft)
+        ft = np.indices(data.shape, dtype=np.int32)
+        ndimage.distance_transform_edt(
+            data, return_distances=False, return_indices=True, indices=ft)
+        fts.append(ft)
+        dt, ft = ndimage.distance_transform_edt(
+            data, return_indices=True)
+        dts.append(dt)
+        fts.append(ft)
+        dt = np.zeros(data.shape, dtype=np.float64)
+        ft = ndimage.distance_transform_edt(
+            data, distances=dt, return_indices=True)
+        dts.append(dt)
+        fts.append(ft)
+        ft = np.indices(data.shape, dtype=np.int32)
+        dt = ndimage.distance_transform_edt(
+            data, return_indices=True, indices=ft)
+        dts.append(dt)
+        fts.append(ft)
+        dt = np.zeros(data.shape, dtype=np.float64)
+        ft = np.indices(data.shape, dtype=np.int32)
+        ndimage.distance_transform_edt(
+            data, distances=dt, return_indices=True, indices=ft)
+        dts.append(dt)
+        fts.append(ft)
+        for dt in dts:
+            assert_array_almost_equal(tdt, dt)
+        for ft in fts:
+            assert_array_almost_equal(tft, ft)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_edt03(self, dtype):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        ref = ndimage.distance_transform_bf(data, 'euclidean', sampling=[2, 2])
+        out = ndimage.distance_transform_edt(data, sampling=[2, 2])
+        assert_array_almost_equal(ref, out)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_distance_transform_edt4(self, dtype):
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        ref = ndimage.distance_transform_bf(data, 'euclidean', sampling=[2, 1])
+        out = ndimage.distance_transform_edt(data, sampling=[2, 1])
+        assert_array_almost_equal(ref, out)
+
+    def test_distance_transform_edt5(self):
+        # Ticket #954 regression test
+        out = ndimage.distance_transform_edt(False)
+        assert_array_almost_equal(out, [0.])
+
+    def test_distance_transform_edt6(self):
+        # test input validation per discussion on PR #13302
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0, 0]])
+        distances_out = np.zeros(data.shape, dtype=np.float64)
+        with assert_raises(RuntimeError):
+            ndimage.distance_transform_bf(
+                data,
+                return_indices=True,
+                return_distances=False,
+                distances=distances_out
+            )
+
+    def test_generate_structure01(self):
+        struct = ndimage.generate_binary_structure(0, 1)
+        assert_array_almost_equal(struct, 1)
+
+    def test_generate_structure02(self):
+        struct = ndimage.generate_binary_structure(1, 1)
+        assert_array_almost_equal(struct, [1, 1, 1])
+
+    def test_generate_structure03(self):
+        struct = ndimage.generate_binary_structure(2, 1)
+        assert_array_almost_equal(struct, [[0, 1, 0],
+                                           [1, 1, 1],
+                                           [0, 1, 0]])
+
+    def test_generate_structure04(self):
+        struct = ndimage.generate_binary_structure(2, 2)
+        assert_array_almost_equal(struct, [[1, 1, 1],
+                                           [1, 1, 1],
+                                           [1, 1, 1]])
+
+    def test_iterate_structure01(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        out = ndimage.iterate_structure(struct, 2)
+        assert_array_almost_equal(out, [[0, 0, 1, 0, 0],
+                                        [0, 1, 1, 1, 0],
+                                        [1, 1, 1, 1, 1],
+                                        [0, 1, 1, 1, 0],
+                                        [0, 0, 1, 0, 0]])
+
+    def test_iterate_structure02(self):
+        struct = [[0, 1],
+                  [1, 1],
+                  [0, 1]]
+        out = ndimage.iterate_structure(struct, 2)
+        assert_array_almost_equal(out, [[0, 0, 1],
+                                        [0, 1, 1],
+                                        [1, 1, 1],
+                                        [0, 1, 1],
+                                        [0, 0, 1]])
+
+    def test_iterate_structure03(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        out = ndimage.iterate_structure(struct, 2, 1)
+        expected = [[0, 0, 1, 0, 0],
+                    [0, 1, 1, 1, 0],
+                    [1, 1, 1, 1, 1],
+                    [0, 1, 1, 1, 0],
+                    [0, 0, 1, 0, 0]]
+        assert_array_almost_equal(out[0], expected)
+        assert_equal(out[1], [2, 2])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion01(self, dtype):
+        data = np.ones([], dtype)
+        out = ndimage.binary_erosion(data)
+        assert_array_almost_equal(out, 1)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion02(self, dtype):
+        data = np.ones([], dtype)
+        out = ndimage.binary_erosion(data, border_value=1)
+        assert_array_almost_equal(out, 1)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion03(self, dtype):
+        data = np.ones([1], dtype)
+        out = ndimage.binary_erosion(data)
+        assert_array_almost_equal(out, [0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion04(self, dtype):
+        data = np.ones([1], dtype)
+        out = ndimage.binary_erosion(data, border_value=1)
+        assert_array_almost_equal(out, [1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion05(self, dtype):
+        data = np.ones([3], dtype)
+        out = ndimage.binary_erosion(data)
+        assert_array_almost_equal(out, [0, 1, 0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion06(self, dtype):
+        data = np.ones([3], dtype)
+        out = ndimage.binary_erosion(data, border_value=1)
+        assert_array_almost_equal(out, [1, 1, 1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion07(self, dtype):
+        data = np.ones([5], dtype)
+        out = ndimage.binary_erosion(data)
+        assert_array_almost_equal(out, [0, 1, 1, 1, 0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion08(self, dtype):
+        data = np.ones([5], dtype)
+        out = ndimage.binary_erosion(data, border_value=1)
+        assert_array_almost_equal(out, [1, 1, 1, 1, 1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion09(self, dtype):
+        data = np.ones([5], dtype)
+        data[2] = 0
+        out = ndimage.binary_erosion(data)
+        assert_array_almost_equal(out, [0, 0, 0, 0, 0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion10(self, dtype):
+        data = np.ones([5], dtype)
+        data[2] = 0
+        out = ndimage.binary_erosion(data, border_value=1)
+        assert_array_almost_equal(out, [1, 0, 0, 0, 1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion11(self, dtype):
+        data = np.ones([5], dtype)
+        data[2] = 0
+        struct = [1, 0, 1]
+        out = ndimage.binary_erosion(data, struct, border_value=1)
+        assert_array_almost_equal(out, [1, 0, 1, 0, 1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion12(self, dtype):
+        data = np.ones([5], dtype)
+        data[2] = 0
+        struct = [1, 0, 1]
+        out = ndimage.binary_erosion(data, struct, border_value=1, origin=-1)
+        assert_array_almost_equal(out, [0, 1, 0, 1, 1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion13(self, dtype):
+        data = np.ones([5], dtype)
+        data[2] = 0
+        struct = [1, 0, 1]
+        out = ndimage.binary_erosion(data, struct, border_value=1, origin=1)
+        assert_array_almost_equal(out, [1, 1, 0, 1, 0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion14(self, dtype):
+        data = np.ones([5], dtype)
+        data[2] = 0
+        struct = [1, 1]
+        out = ndimage.binary_erosion(data, struct, border_value=1)
+        assert_array_almost_equal(out, [1, 1, 0, 0, 1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion15(self, dtype):
+        data = np.ones([5], dtype)
+        data[2] = 0
+        struct = [1, 1]
+        out = ndimage.binary_erosion(data, struct, border_value=1, origin=-1)
+        assert_array_almost_equal(out, [1, 0, 0, 1, 1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion16(self, dtype):
+        data = np.ones([1, 1], dtype)
+        out = ndimage.binary_erosion(data, border_value=1)
+        assert_array_almost_equal(out, [[1]])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion17(self, dtype):
+        data = np.ones([1, 1], dtype)
+        out = ndimage.binary_erosion(data)
+        assert_array_almost_equal(out, [[0]])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion18(self, dtype):
+        data = np.ones([1, 3], dtype)
+        out = ndimage.binary_erosion(data)
+        assert_array_almost_equal(out, [[0, 0, 0]])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion19(self, dtype):
+        data = np.ones([1, 3], dtype)
+        out = ndimage.binary_erosion(data, border_value=1)
+        assert_array_almost_equal(out, [[1, 1, 1]])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion20(self, dtype):
+        data = np.ones([3, 3], dtype)
+        out = ndimage.binary_erosion(data)
+        assert_array_almost_equal(out, [[0, 0, 0],
+                                        [0, 1, 0],
+                                        [0, 0, 0]])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion21(self, dtype):
+        data = np.ones([3, 3], dtype)
+        out = ndimage.binary_erosion(data, border_value=1)
+        assert_array_almost_equal(out, [[1, 1, 1],
+                                        [1, 1, 1],
+                                        [1, 1, 1]])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion22(self, dtype):
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 1, 0, 0],
+                    [0, 0, 0, 1, 1, 0, 0, 0],
+                    [0, 0, 1, 0, 0, 1, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 1, 1],
+                         [0, 0, 1, 1, 1, 1, 1, 1],
+                         [0, 0, 1, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 1, 0, 0, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_erosion(data, border_value=1)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion23(self, dtype):
+        struct = ndimage.generate_binary_structure(2, 2)
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 1, 1],
+                         [0, 0, 1, 1, 1, 1, 1, 1],
+                         [0, 0, 1, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 1, 0, 0, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_erosion(data, struct, border_value=1)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion24(self, dtype):
+        struct = [[0, 1],
+                  [1, 1]]
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 1, 1, 1],
+                    [0, 0, 0, 1, 1, 1, 0, 0],
+                    [0, 0, 1, 1, 1, 1, 0, 0],
+                    [0, 0, 1, 0, 0, 0, 1, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 1, 1],
+                         [0, 0, 1, 1, 1, 1, 1, 1],
+                         [0, 0, 1, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 1, 0, 0, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_erosion(data, struct, border_value=1)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion25(self, dtype):
+        struct = [[0, 1, 0],
+                  [1, 0, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 1, 0, 0],
+                    [0, 0, 0, 1, 0, 0, 0, 0],
+                    [0, 0, 1, 0, 0, 1, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 1, 1],
+                         [0, 0, 1, 1, 1, 0, 1, 1],
+                         [0, 0, 1, 0, 1, 1, 0, 0],
+                         [0, 1, 0, 1, 1, 1, 1, 0],
+                         [0, 1, 1, 0, 0, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_erosion(data, struct, border_value=1)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_erosion26(self, dtype):
+        struct = [[0, 1, 0],
+                  [1, 0, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 1],
+                    [0, 0, 0, 0, 1, 0, 0, 1],
+                    [0, 0, 1, 0, 0, 0, 0, 0],
+                    [0, 1, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 1]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 1, 1],
+                         [0, 0, 1, 1, 1, 0, 1, 1],
+                         [0, 0, 1, 0, 1, 1, 0, 0],
+                         [0, 1, 0, 1, 1, 1, 1, 0],
+                         [0, 1, 1, 0, 0, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_erosion(data, struct, border_value=1,
+                                     origin=(-1, -1))
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_erosion27(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0]], bool)
+        out = ndimage.binary_erosion(data, struct, border_value=1,
+                                     iterations=2)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_erosion28(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0]], bool)
+        out = np.zeros(data.shape, bool)
+        ndimage.binary_erosion(data, struct, border_value=1,
+                               iterations=2, output=out)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_erosion29(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [1, 1, 1, 1, 1, 1, 1],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0]], bool)
+        out = ndimage.binary_erosion(data, struct,
+                                     border_value=1, iterations=3)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_erosion30(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [1, 1, 1, 1, 1, 1, 1],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0]], bool)
+        out = np.zeros(data.shape, bool)
+        ndimage.binary_erosion(data, struct, border_value=1,
+                               iterations=3, output=out)
+        assert_array_almost_equal(out, expected)
+
+        # test with output memory overlap
+        ndimage.binary_erosion(data, struct, border_value=1,
+                               iterations=3, output=data)
+        assert_array_almost_equal(data, expected)
+
+    def test_binary_erosion31(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 1, 0, 0, 0, 0],
+                    [0, 1, 1, 1, 0, 0, 0],
+                    [1, 1, 1, 1, 1, 0, 1],
+                    [0, 1, 1, 1, 0, 0, 0],
+                    [0, 0, 1, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 1, 0, 0, 0, 1]]
+        data = np.array([[0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [1, 1, 1, 1, 1, 1, 1],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0]], bool)
+        out = np.zeros(data.shape, bool)
+        ndimage.binary_erosion(data, struct, border_value=1,
+                               iterations=1, output=out, origin=(-1, -1))
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_erosion32(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0]], bool)
+        out = ndimage.binary_erosion(data, struct,
+                                     border_value=1, iterations=2)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_erosion33(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0, 1, 1],
+                    [0, 0, 0, 0, 0, 0, 1],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0]]
+        mask = [[1, 1, 1, 1, 1, 0, 0],
+                [1, 1, 1, 1, 1, 1, 0],
+                [1, 1, 1, 1, 1, 1, 1],
+                [1, 1, 1, 1, 1, 1, 1],
+                [1, 1, 1, 1, 1, 1, 1],
+                [1, 1, 1, 1, 1, 1, 1],
+                [1, 1, 1, 1, 1, 1, 1]]
+        data = np.array([[0, 0, 0, 0, 0, 1, 1],
+                         [0, 0, 0, 1, 0, 0, 1],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0]], bool)
+        out = ndimage.binary_erosion(data, struct,
+                                     border_value=1, mask=mask, iterations=-1)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_erosion34(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 0, 1, 0, 0, 0],
+                    [0, 1, 1, 1, 1, 1, 0],
+                    [0, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0]]
+        mask = [[0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 1, 1, 1, 0, 0],
+                [0, 0, 1, 0, 1, 0, 0],
+                [0, 0, 1, 1, 1, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0]], bool)
+        out = ndimage.binary_erosion(data, struct,
+                                     border_value=1, mask=mask)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_erosion35(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        mask = [[0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 1, 1, 1, 0, 0],
+                [0, 0, 1, 0, 1, 0, 0],
+                [0, 0, 1, 1, 1, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [1, 1, 1, 1, 1, 1, 1],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0]], bool)
+        tmp = [[0, 0, 1, 0, 0, 0, 0],
+               [0, 1, 1, 1, 0, 0, 0],
+               [1, 1, 1, 1, 1, 0, 1],
+               [0, 1, 1, 1, 0, 0, 0],
+               [0, 0, 1, 0, 0, 0, 0],
+               [0, 0, 0, 0, 0, 0, 0],
+               [0, 0, 1, 0, 0, 0, 1]]
+        expected = np.logical_and(tmp, mask)
+        tmp = np.logical_and(data, np.logical_not(mask))
+        expected = np.logical_or(expected, tmp)
+        out = np.zeros(data.shape, bool)
+        ndimage.binary_erosion(data, struct, border_value=1,
+                               iterations=1, output=out,
+                               origin=(-1, -1), mask=mask)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_erosion36(self):
+        struct = [[0, 1, 0],
+                  [1, 0, 1],
+                  [0, 1, 0]]
+        mask = [[0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 1, 1, 1, 0, 0, 0],
+                [0, 0, 1, 0, 1, 0, 0, 0],
+                [0, 0, 1, 1, 1, 0, 0, 0],
+                [0, 0, 1, 1, 1, 0, 0, 0],
+                [0, 0, 1, 1, 1, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0]]
+        tmp = [[0, 0, 0, 0, 0, 0, 0, 0],
+               [0, 0, 0, 0, 0, 0, 0, 1],
+               [0, 0, 0, 0, 1, 0, 0, 1],
+               [0, 0, 1, 0, 0, 0, 0, 0],
+               [0, 1, 0, 0, 1, 0, 0, 0],
+               [0, 0, 0, 0, 0, 0, 0, 0],
+               [0, 0, 0, 0, 0, 0, 0, 0],
+               [0, 0, 0, 0, 0, 0, 0, 1]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 1, 1],
+                         [0, 0, 1, 1, 1, 0, 1, 1],
+                         [0, 0, 1, 0, 1, 1, 0, 0],
+                         [0, 1, 0, 1, 1, 1, 1, 0],
+                         [0, 1, 1, 0, 0, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]])
+        expected = np.logical_and(tmp, mask)
+        tmp = np.logical_and(data, np.logical_not(mask))
+        expected = np.logical_or(expected, tmp)
+        out = ndimage.binary_erosion(data, struct, mask=mask,
+                                     border_value=1, origin=(-1, -1))
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_erosion37(self):
+        a = np.array([[1, 0, 1],
+                      [0, 1, 0],
+                      [1, 0, 1]], dtype=bool)
+        b = np.zeros_like(a)
+        out = ndimage.binary_erosion(a, structure=a, output=b, iterations=0,
+                                     border_value=True, brute_force=True)
+        assert_(out is b)
+        assert_array_equal(
+            ndimage.binary_erosion(a, structure=a, iterations=0,
+                                   border_value=True),
+            b)
+
+    def test_binary_erosion38(self):
+        data = np.array([[1, 0, 1],
+                        [0, 1, 0],
+                        [1, 0, 1]], dtype=bool)
+        iterations = 2.0
+        with assert_raises(TypeError):
+            _ = ndimage.binary_erosion(data, iterations=iterations)
+
+    def test_binary_erosion39(self):
+        iterations = np.int32(3)
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [1, 1, 1, 1, 1, 1, 1],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0]], bool)
+        out = np.zeros(data.shape, bool)
+        ndimage.binary_erosion(data, struct, border_value=1,
+                               iterations=iterations, output=out)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_erosion40(self):
+        iterations = np.int64(3)
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 1, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [1, 1, 1, 1, 1, 1, 1],
+                         [0, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 1, 0, 0, 0]], bool)
+        out = np.zeros(data.shape, bool)
+        ndimage.binary_erosion(data, struct, border_value=1,
+                               iterations=iterations, output=out)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation01(self, dtype):
+        data = np.ones([], dtype)
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, 1)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation02(self, dtype):
+        data = np.zeros([], dtype)
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, 0)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation03(self, dtype):
+        data = np.ones([1], dtype)
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, [1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation04(self, dtype):
+        data = np.zeros([1], dtype)
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, [0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation05(self, dtype):
+        data = np.ones([3], dtype)
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, [1, 1, 1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation06(self, dtype):
+        data = np.zeros([3], dtype)
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, [0, 0, 0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation07(self, dtype):
+        data = np.zeros([3], dtype)
+        data[1] = 1
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, [1, 1, 1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation08(self, dtype):
+        data = np.zeros([5], dtype)
+        data[1] = 1
+        data[3] = 1
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, [1, 1, 1, 1, 1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation09(self, dtype):
+        data = np.zeros([5], dtype)
+        data[1] = 1
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, [1, 1, 1, 0, 0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation10(self, dtype):
+        data = np.zeros([5], dtype)
+        data[1] = 1
+        out = ndimage.binary_dilation(data, origin=-1)
+        assert_array_almost_equal(out, [0, 1, 1, 1, 0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation11(self, dtype):
+        data = np.zeros([5], dtype)
+        data[1] = 1
+        out = ndimage.binary_dilation(data, origin=1)
+        assert_array_almost_equal(out, [1, 1, 0, 0, 0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation12(self, dtype):
+        data = np.zeros([5], dtype)
+        data[1] = 1
+        struct = [1, 0, 1]
+        out = ndimage.binary_dilation(data, struct)
+        assert_array_almost_equal(out, [1, 0, 1, 0, 0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation13(self, dtype):
+        data = np.zeros([5], dtype)
+        data[1] = 1
+        struct = [1, 0, 1]
+        out = ndimage.binary_dilation(data, struct, border_value=1)
+        assert_array_almost_equal(out, [1, 0, 1, 0, 1])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation14(self, dtype):
+        data = np.zeros([5], dtype)
+        data[1] = 1
+        struct = [1, 0, 1]
+        out = ndimage.binary_dilation(data, struct, origin=-1)
+        assert_array_almost_equal(out, [0, 1, 0, 1, 0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation15(self, dtype):
+        data = np.zeros([5], dtype)
+        data[1] = 1
+        struct = [1, 0, 1]
+        out = ndimage.binary_dilation(data, struct,
+                                      origin=-1, border_value=1)
+        assert_array_almost_equal(out, [1, 1, 0, 1, 0])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation16(self, dtype):
+        data = np.ones([1, 1], dtype)
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, [[1]])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation17(self, dtype):
+        data = np.zeros([1, 1], dtype)
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, [[0]])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation18(self, dtype):
+        data = np.ones([1, 3], dtype)
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, [[1, 1, 1]])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation19(self, dtype):
+        data = np.ones([3, 3], dtype)
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, [[1, 1, 1],
+                                        [1, 1, 1],
+                                        [1, 1, 1]])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation20(self, dtype):
+        data = np.zeros([3, 3], dtype)
+        data[1, 1] = 1
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, [[0, 1, 0],
+                                        [1, 1, 1],
+                                        [0, 1, 0]])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation21(self, dtype):
+        struct = ndimage.generate_binary_structure(2, 2)
+        data = np.zeros([3, 3], dtype)
+        data[1, 1] = 1
+        out = ndimage.binary_dilation(data, struct)
+        assert_array_almost_equal(out, [[1, 1, 1],
+                                        [1, 1, 1],
+                                        [1, 1, 1]])
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation22(self, dtype):
+        expected = [[0, 1, 0, 0, 0, 0, 0, 0],
+                    [1, 1, 1, 0, 0, 0, 0, 0],
+                    [0, 1, 0, 0, 0, 1, 0, 0],
+                    [0, 0, 0, 1, 1, 1, 1, 0],
+                    [0, 0, 1, 1, 1, 1, 0, 0],
+                    [0, 1, 1, 1, 1, 1, 1, 0],
+                    [0, 0, 1, 0, 0, 1, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_dilation(data)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation23(self, dtype):
+        expected = [[1, 1, 1, 1, 1, 1, 1, 1],
+                    [1, 1, 1, 0, 0, 0, 0, 1],
+                    [1, 1, 0, 0, 0, 1, 0, 1],
+                    [1, 0, 0, 1, 1, 1, 1, 1],
+                    [1, 0, 1, 1, 1, 1, 0, 1],
+                    [1, 1, 1, 1, 1, 1, 1, 1],
+                    [1, 0, 1, 0, 0, 1, 0, 1],
+                    [1, 1, 1, 1, 1, 1, 1, 1]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_dilation(data, border_value=1)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation24(self, dtype):
+        expected = [[1, 1, 0, 0, 0, 0, 0, 0],
+                    [1, 0, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 1, 1, 1, 1, 0, 0],
+                    [0, 1, 1, 1, 1, 0, 0, 0],
+                    [1, 1, 1, 1, 1, 1, 0, 0],
+                    [0, 1, 0, 0, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_dilation(data, origin=(1, 1))
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation25(self, dtype):
+        expected = [[1, 1, 0, 0, 0, 0, 1, 1],
+                    [1, 0, 0, 0, 1, 0, 1, 1],
+                    [0, 0, 1, 1, 1, 1, 1, 1],
+                    [0, 1, 1, 1, 1, 0, 1, 1],
+                    [1, 1, 1, 1, 1, 1, 1, 1],
+                    [0, 1, 0, 0, 1, 0, 1, 1],
+                    [1, 1, 1, 1, 1, 1, 1, 1],
+                    [1, 1, 1, 1, 1, 1, 1, 1]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_dilation(data, origin=(1, 1), border_value=1)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation26(self, dtype):
+        struct = ndimage.generate_binary_structure(2, 2)
+        expected = [[1, 1, 1, 0, 0, 0, 0, 0],
+                    [1, 1, 1, 0, 0, 0, 0, 0],
+                    [1, 1, 1, 0, 1, 1, 1, 0],
+                    [0, 0, 1, 1, 1, 1, 1, 0],
+                    [0, 1, 1, 1, 1, 1, 1, 0],
+                    [0, 1, 1, 1, 1, 1, 1, 0],
+                    [0, 1, 1, 1, 1, 1, 1, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_dilation(data, struct)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation27(self, dtype):
+        struct = [[0, 1],
+                  [1, 1]]
+        expected = [[0, 1, 0, 0, 0, 0, 0, 0],
+                    [1, 1, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 1, 0, 0],
+                    [0, 0, 0, 1, 1, 1, 0, 0],
+                    [0, 0, 1, 1, 1, 1, 0, 0],
+                    [0, 1, 1, 0, 1, 1, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_dilation(data, struct)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation28(self, dtype):
+        expected = [[1, 1, 1, 1],
+                    [1, 0, 0, 1],
+                    [1, 0, 0, 1],
+                    [1, 1, 1, 1]]
+        data = np.array([[0, 0, 0, 0],
+                         [0, 0, 0, 0],
+                         [0, 0, 0, 0],
+                         [0, 0, 0, 0]], dtype)
+        out = ndimage.binary_dilation(data, border_value=1)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_dilation29(self):
+        struct = [[0, 1],
+                  [1, 1]]
+        expected = [[0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 0],
+                    [0, 0, 1, 1, 0],
+                    [0, 1, 1, 1, 0],
+                    [0, 0, 0, 0, 0]]
+
+        data = np.array([[0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 0],
+                         [0, 0, 0, 0, 0]], bool)
+        out = ndimage.binary_dilation(data, struct, iterations=2)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_dilation30(self):
+        struct = [[0, 1],
+                  [1, 1]]
+        expected = [[0, 0, 0, 0, 0],
+                    [0, 0, 0, 1, 0],
+                    [0, 0, 1, 1, 0],
+                    [0, 1, 1, 1, 0],
+                    [0, 0, 0, 0, 0]]
+
+        data = np.array([[0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 0],
+                         [0, 0, 0, 0, 0]], bool)
+        out = np.zeros(data.shape, bool)
+        ndimage.binary_dilation(data, struct, iterations=2, output=out)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_dilation31(self):
+        struct = [[0, 1],
+                  [1, 1]]
+        expected = [[0, 0, 0, 1, 0],
+                    [0, 0, 1, 1, 0],
+                    [0, 1, 1, 1, 0],
+                    [1, 1, 1, 1, 0],
+                    [0, 0, 0, 0, 0]]
+
+        data = np.array([[0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 0],
+                         [0, 0, 0, 0, 0]], bool)
+        out = ndimage.binary_dilation(data, struct, iterations=3)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_dilation32(self):
+        struct = [[0, 1],
+                  [1, 1]]
+        expected = [[0, 0, 0, 1, 0],
+                    [0, 0, 1, 1, 0],
+                    [0, 1, 1, 1, 0],
+                    [1, 1, 1, 1, 0],
+                    [0, 0, 0, 0, 0]]
+
+        data = np.array([[0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 0],
+                         [0, 0, 0, 0, 0]], bool)
+        out = np.zeros(data.shape, bool)
+        ndimage.binary_dilation(data, struct, iterations=3, output=out)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_dilation33(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = np.array([[0, 1, 0, 0, 0, 0, 0, 0],
+                             [0, 0, 0, 0, 0, 0, 0, 0],
+                             [0, 0, 0, 0, 0, 0, 0, 0],
+                             [0, 0, 0, 0, 1, 1, 0, 0],
+                             [0, 0, 1, 1, 1, 0, 0, 0],
+                             [0, 1, 1, 0, 1, 1, 0, 0],
+                             [0, 0, 0, 0, 0, 0, 0, 0],
+                             [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+        mask = np.array([[0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 1, 0],
+                         [0, 0, 0, 0, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 1, 1, 0, 1, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+        data = np.array([[0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                           [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                           [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+
+        out = ndimage.binary_dilation(data, struct, iterations=-1,
+                                      mask=mask, border_value=0)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_dilation34(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 1, 0, 0, 0, 0, 0, 0],
+                    [0, 1, 1, 0, 0, 0, 0, 0],
+                    [0, 0, 1, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        mask = np.array([[0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 1, 0, 0, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+        data = np.zeros(mask.shape, bool)
+        out = ndimage.binary_dilation(data, struct, iterations=-1,
+                                      mask=mask, border_value=1)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_dilation35(self, dtype):
+        tmp = [[1, 1, 0, 0, 0, 0, 1, 1],
+               [1, 0, 0, 0, 1, 0, 1, 1],
+               [0, 0, 1, 1, 1, 1, 1, 1],
+               [0, 1, 1, 1, 1, 0, 1, 1],
+               [1, 1, 1, 1, 1, 1, 1, 1],
+               [0, 1, 0, 0, 1, 0, 1, 1],
+               [1, 1, 1, 1, 1, 1, 1, 1],
+               [1, 1, 1, 1, 1, 1, 1, 1]]
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]])
+        mask = [[0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 1, 1, 1, 1, 0, 0],
+                [0, 0, 1, 1, 1, 1, 0, 0],
+                [0, 0, 1, 1, 1, 1, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0]]
+        expected = np.logical_and(tmp, mask)
+        tmp = np.logical_and(data, np.logical_not(mask))
+        expected = np.logical_or(expected, tmp)
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_dilation(data, mask=mask,
+                                      origin=(1, 1), border_value=1)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_propagation01(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = np.array([[0, 1, 0, 0, 0, 0, 0, 0],
+                             [0, 0, 0, 0, 0, 0, 0, 0],
+                             [0, 0, 0, 0, 0, 0, 0, 0],
+                             [0, 0, 0, 0, 1, 1, 0, 0],
+                             [0, 0, 1, 1, 1, 0, 0, 0],
+                             [0, 1, 1, 0, 1, 1, 0, 0],
+                             [0, 0, 0, 0, 0, 0, 0, 0],
+                             [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+        mask = np.array([[0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 1, 0],
+                         [0, 0, 0, 0, 1, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 0, 0, 0],
+                         [0, 1, 1, 0, 1, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+        data = np.array([[0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+
+        out = ndimage.binary_propagation(data, struct,
+                                         mask=mask, border_value=0)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_propagation02(self):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 1, 0, 0, 0, 0, 0, 0],
+                    [0, 1, 1, 0, 0, 0, 0, 0],
+                    [0, 0, 1, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        mask = np.array([[0, 1, 0, 0, 0, 0, 0, 0],
+                         [0, 1, 1, 0, 0, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+        data = np.zeros(mask.shape, bool)
+        out = ndimage.binary_propagation(data, struct,
+                                         mask=mask, border_value=1)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_opening01(self, dtype):
+        expected = [[0, 1, 0, 0, 0, 0, 0, 0],
+                    [1, 1, 1, 0, 0, 0, 0, 0],
+                    [0, 1, 0, 0, 0, 1, 0, 0],
+                    [0, 0, 0, 0, 1, 1, 1, 0],
+                    [0, 0, 1, 0, 0, 1, 0, 0],
+                    [0, 1, 1, 1, 1, 1, 1, 0],
+                    [0, 0, 1, 0, 0, 1, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 1, 0, 0, 0, 0, 0, 0],
+                         [1, 1, 1, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 0, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_opening(data)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_opening02(self, dtype):
+        struct = ndimage.generate_binary_structure(2, 2)
+        expected = [[1, 1, 1, 0, 0, 0, 0, 0],
+                    [1, 1, 1, 0, 0, 0, 0, 0],
+                    [1, 1, 1, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 1, 1, 1, 0, 0, 0, 0],
+                    [0, 1, 1, 1, 0, 0, 0, 0],
+                    [0, 1, 1, 1, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[1, 1, 1, 0, 0, 0, 0, 0],
+                         [1, 1, 1, 0, 0, 0, 0, 0],
+                         [1, 1, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 1, 1, 0, 1, 1, 0],
+                         [0, 1, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_opening(data, struct)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_closing01(self, dtype):
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 1, 1, 0, 0, 0, 0, 0],
+                    [0, 1, 1, 1, 0, 1, 0, 0],
+                    [0, 0, 1, 1, 1, 1, 1, 0],
+                    [0, 0, 1, 1, 1, 1, 0, 0],
+                    [0, 1, 1, 1, 1, 1, 1, 0],
+                    [0, 0, 1, 0, 0, 1, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 1, 0, 0, 0, 0, 0, 0],
+                         [1, 1, 1, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 0, 1, 0, 0],
+                         [0, 1, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_closing(data)
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_binary_closing02(self, dtype):
+        struct = ndimage.generate_binary_structure(2, 2)
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 1, 1, 0, 0, 0, 0, 0],
+                    [0, 1, 1, 1, 1, 1, 1, 0],
+                    [0, 1, 1, 1, 1, 1, 1, 0],
+                    [0, 1, 1, 1, 1, 1, 1, 0],
+                    [0, 1, 1, 1, 1, 1, 1, 0],
+                    [0, 1, 1, 1, 1, 1, 1, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[1, 1, 1, 0, 0, 0, 0, 0],
+                         [1, 1, 1, 0, 0, 0, 0, 0],
+                         [1, 1, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 1, 1, 0, 1, 1, 0],
+                         [0, 1, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_closing(data, struct)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_fill_holes01(self):
+        expected = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                             [0, 0, 1, 1, 1, 1, 0, 0],
+                             [0, 0, 1, 1, 1, 1, 0, 0],
+                             [0, 0, 1, 1, 1, 1, 0, 0],
+                             [0, 0, 1, 1, 1, 1, 0, 0],
+                             [0, 0, 1, 1, 1, 1, 0, 0],
+                             [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 1, 1, 1, 1, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+        out = ndimage.binary_fill_holes(data)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_fill_holes02(self):
+        expected = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                             [0, 0, 0, 1, 1, 0, 0, 0],
+                             [0, 0, 1, 1, 1, 1, 0, 0],
+                             [0, 0, 1, 1, 1, 1, 0, 0],
+                             [0, 0, 1, 1, 1, 1, 0, 0],
+                             [0, 0, 0, 1, 1, 0, 0, 0],
+                             [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 0, 1, 1, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 1, 0, 0, 1, 0, 0],
+                         [0, 0, 0, 1, 1, 0, 0, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+        out = ndimage.binary_fill_holes(data)
+        assert_array_almost_equal(out, expected)
+
+    def test_binary_fill_holes03(self):
+        expected = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                             [0, 0, 1, 0, 0, 0, 0, 0],
+                             [0, 1, 1, 1, 0, 1, 1, 1],
+                             [0, 1, 1, 1, 0, 1, 1, 1],
+                             [0, 1, 1, 1, 0, 1, 1, 1],
+                             [0, 0, 1, 0, 0, 1, 1, 1],
+                             [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+        data = np.array([[0, 0, 0, 0, 0, 0, 0, 0],
+                         [0, 0, 1, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 1, 0, 1, 1, 1],
+                         [0, 1, 0, 1, 0, 1, 0, 1],
+                         [0, 1, 0, 1, 0, 1, 0, 1],
+                         [0, 0, 1, 0, 0, 1, 1, 1],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], bool)
+        out = ndimage.binary_fill_holes(data)
+        assert_array_almost_equal(out, expected)
+
+    def test_grey_erosion01(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        output = ndimage.grey_erosion(array, footprint=footprint)
+        assert_array_almost_equal([[2, 2, 1, 1, 1],
+                                   [2, 3, 1, 3, 1],
+                                   [5, 5, 3, 3, 1]], output)
+
+    def test_grey_erosion01_overlap(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                           [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        ndimage.grey_erosion(array, footprint=footprint, output=array)
+        assert_array_almost_equal([[2, 2, 1, 1, 1],
+                                   [2, 3, 1, 3, 1],
+                                   [5, 5, 3, 3, 1]], array)
+
+    def test_grey_erosion02(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        structure = [[0, 0, 0], [0, 0, 0]]
+        output = ndimage.grey_erosion(array, footprint=footprint,
+                                      structure=structure)
+        assert_array_almost_equal([[2, 2, 1, 1, 1],
+                                   [2, 3, 1, 3, 1],
+                                   [5, 5, 3, 3, 1]], output)
+
+    def test_grey_erosion03(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        structure = [[1, 1, 1], [1, 1, 1]]
+        output = ndimage.grey_erosion(array, footprint=footprint,
+                                      structure=structure)
+        assert_array_almost_equal([[1, 1, 0, 0, 0],
+                                   [1, 2, 0, 2, 0],
+                                   [4, 4, 2, 2, 0]], output)
+
+    def test_grey_dilation01(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[0, 1, 1], [1, 0, 1]]
+        output = ndimage.grey_dilation(array, footprint=footprint)
+        assert_array_almost_equal([[7, 7, 9, 9, 5],
+                                   [7, 9, 8, 9, 7],
+                                   [8, 8, 8, 7, 7]], output)
+
+    def test_grey_dilation02(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[0, 1, 1], [1, 0, 1]]
+        structure = [[0, 0, 0], [0, 0, 0]]
+        output = ndimage.grey_dilation(array, footprint=footprint,
+                                       structure=structure)
+        assert_array_almost_equal([[7, 7, 9, 9, 5],
+                                   [7, 9, 8, 9, 7],
+                                   [8, 8, 8, 7, 7]], output)
+
+    def test_grey_dilation03(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[0, 1, 1], [1, 0, 1]]
+        structure = [[1, 1, 1], [1, 1, 1]]
+        output = ndimage.grey_dilation(array, footprint=footprint,
+                                       structure=structure)
+        assert_array_almost_equal([[8, 8, 10, 10, 6],
+                                   [8, 10, 9, 10, 8],
+                                   [9, 9, 9, 8, 8]], output)
+
+    def test_grey_opening01(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        tmp = ndimage.grey_erosion(array, footprint=footprint)
+        expected = ndimage.grey_dilation(tmp, footprint=footprint)
+        output = ndimage.grey_opening(array, footprint=footprint)
+        assert_array_almost_equal(expected, output)
+
+    def test_grey_opening02(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        structure = [[0, 0, 0], [0, 0, 0]]
+        tmp = ndimage.grey_erosion(array, footprint=footprint,
+                                   structure=structure)
+        expected = ndimage.grey_dilation(tmp, footprint=footprint,
+                                         structure=structure)
+        output = ndimage.grey_opening(array, footprint=footprint,
+                                      structure=structure)
+        assert_array_almost_equal(expected, output)
+
+    def test_grey_closing01(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        tmp = ndimage.grey_dilation(array, footprint=footprint)
+        expected = ndimage.grey_erosion(tmp, footprint=footprint)
+        output = ndimage.grey_closing(array, footprint=footprint)
+        assert_array_almost_equal(expected, output)
+
+    def test_grey_closing02(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        structure = [[0, 0, 0], [0, 0, 0]]
+        tmp = ndimage.grey_dilation(array, footprint=footprint,
+                                    structure=structure)
+        expected = ndimage.grey_erosion(tmp, footprint=footprint,
+                                        structure=structure)
+        output = ndimage.grey_closing(array, footprint=footprint,
+                                      structure=structure)
+        assert_array_almost_equal(expected, output)
+
+    def test_morphological_gradient01(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        structure = [[0, 0, 0], [0, 0, 0]]
+        tmp1 = ndimage.grey_dilation(array, footprint=footprint,
+                                     structure=structure)
+        tmp2 = ndimage.grey_erosion(array, footprint=footprint,
+                                    structure=structure)
+        expected = tmp1 - tmp2
+        output = np.zeros(array.shape, array.dtype)
+        ndimage.morphological_gradient(array, footprint=footprint,
+                                       structure=structure, output=output)
+        assert_array_almost_equal(expected, output)
+
+    def test_morphological_gradient02(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        structure = [[0, 0, 0], [0, 0, 0]]
+        tmp1 = ndimage.grey_dilation(array, footprint=footprint,
+                                     structure=structure)
+        tmp2 = ndimage.grey_erosion(array, footprint=footprint,
+                                    structure=structure)
+        expected = tmp1 - tmp2
+        output = ndimage.morphological_gradient(array, footprint=footprint,
+                                                structure=structure)
+        assert_array_almost_equal(expected, output)
+
+    def test_morphological_laplace01(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        structure = [[0, 0, 0], [0, 0, 0]]
+        tmp1 = ndimage.grey_dilation(array, footprint=footprint,
+                                     structure=structure)
+        tmp2 = ndimage.grey_erosion(array, footprint=footprint,
+                                    structure=structure)
+        expected = tmp1 + tmp2 - 2 * array
+        output = np.zeros(array.shape, array.dtype)
+        ndimage.morphological_laplace(array, footprint=footprint,
+                                      structure=structure, output=output)
+        assert_array_almost_equal(expected, output)
+
+    def test_morphological_laplace02(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        structure = [[0, 0, 0], [0, 0, 0]]
+        tmp1 = ndimage.grey_dilation(array, footprint=footprint,
+                                     structure=structure)
+        tmp2 = ndimage.grey_erosion(array, footprint=footprint,
+                                    structure=structure)
+        expected = tmp1 + tmp2 - 2 * array
+        output = ndimage.morphological_laplace(array, footprint=footprint,
+                                               structure=structure)
+        assert_array_almost_equal(expected, output)
+
+    def test_white_tophat01(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        structure = [[0, 0, 0], [0, 0, 0]]
+        tmp = ndimage.grey_opening(array, footprint=footprint,
+                                   structure=structure)
+        expected = array - tmp
+        output = np.zeros(array.shape, array.dtype)
+        ndimage.white_tophat(array, footprint=footprint,
+                             structure=structure, output=output)
+        assert_array_almost_equal(expected, output)
+
+    def test_white_tophat02(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        structure = [[0, 0, 0], [0, 0, 0]]
+        tmp = ndimage.grey_opening(array, footprint=footprint,
+                                   structure=structure)
+        expected = array - tmp
+        output = ndimage.white_tophat(array, footprint=footprint,
+                                      structure=structure)
+        assert_array_almost_equal(expected, output)
+
+    def test_white_tophat03(self):
+        array = np.array([[1, 0, 0, 0, 0, 0, 0],
+                          [0, 1, 1, 1, 1, 1, 0],
+                          [0, 1, 1, 1, 1, 1, 0],
+                          [0, 1, 1, 1, 1, 1, 0],
+                          [0, 1, 1, 1, 0, 1, 0],
+                          [0, 1, 1, 1, 1, 1, 0],
+                          [0, 0, 0, 0, 0, 0, 1]], dtype=np.bool_)
+        structure = np.ones((3, 3), dtype=np.bool_)
+        expected = np.array([[0, 1, 1, 0, 0, 0, 0],
+                             [1, 0, 0, 1, 1, 1, 0],
+                             [1, 0, 0, 1, 1, 1, 0],
+                             [0, 1, 1, 0, 0, 0, 1],
+                             [0, 1, 1, 0, 1, 0, 1],
+                             [0, 1, 1, 0, 0, 0, 1],
+                             [0, 0, 0, 1, 1, 1, 1]], dtype=np.bool_)
+
+        output = ndimage.white_tophat(array, structure=structure)
+        assert_array_equal(expected, output)
+
+    def test_white_tophat04(self):
+        array = np.eye(5, dtype=np.bool_)
+        structure = np.ones((3, 3), dtype=np.bool_)
+
+        # Check that type mismatch is properly handled
+        output = np.empty_like(array, dtype=np.float64)
+        ndimage.white_tophat(array, structure=structure, output=output)
+
+    def test_black_tophat01(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        structure = [[0, 0, 0], [0, 0, 0]]
+        tmp = ndimage.grey_closing(array, footprint=footprint,
+                                   structure=structure)
+        expected = tmp - array
+        output = np.zeros(array.shape, array.dtype)
+        ndimage.black_tophat(array, footprint=footprint,
+                             structure=structure, output=output)
+        assert_array_almost_equal(expected, output)
+
+    def test_black_tophat02(self):
+        array = np.array([[3, 2, 5, 1, 4],
+                          [7, 6, 9, 3, 5],
+                          [5, 8, 3, 7, 1]])
+        footprint = [[1, 0, 1], [1, 1, 0]]
+        structure = [[0, 0, 0], [0, 0, 0]]
+        tmp = ndimage.grey_closing(array, footprint=footprint,
+                                   structure=structure)
+        expected = tmp - array
+        output = ndimage.black_tophat(array, footprint=footprint,
+                                      structure=structure)
+        assert_array_almost_equal(expected, output)
+
+    def test_black_tophat03(self):
+        array = np.array([[1, 0, 0, 0, 0, 0, 0],
+                          [0, 1, 1, 1, 1, 1, 0],
+                          [0, 1, 1, 1, 1, 1, 0],
+                          [0, 1, 1, 1, 1, 1, 0],
+                          [0, 1, 1, 1, 0, 1, 0],
+                          [0, 1, 1, 1, 1, 1, 0],
+                          [0, 0, 0, 0, 0, 0, 1]], dtype=np.bool_)
+        structure = np.ones((3, 3), dtype=np.bool_)
+        expected = np.array([[0, 1, 1, 1, 1, 1, 1],
+                             [1, 0, 0, 0, 0, 0, 1],
+                             [1, 0, 0, 0, 0, 0, 1],
+                             [1, 0, 0, 0, 0, 0, 1],
+                             [1, 0, 0, 0, 1, 0, 1],
+                             [1, 0, 0, 0, 0, 0, 1],
+                             [1, 1, 1, 1, 1, 1, 0]], dtype=np.bool_)
+
+        output = ndimage.black_tophat(array, structure=structure)
+        assert_array_equal(expected, output)
+
+    def test_black_tophat04(self):
+        array = np.eye(5, dtype=np.bool_)
+        structure = np.ones((3, 3), dtype=np.bool_)
+
+        # Check that type mismatch is properly handled
+        output = np.empty_like(array, dtype=np.float64)
+        ndimage.black_tophat(array, structure=structure, output=output)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_hit_or_miss01(self, dtype):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0],
+                    [0, 1, 0, 0, 0],
+                    [0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0]]
+        data = np.array([[0, 1, 0, 0, 0],
+                         [1, 1, 1, 0, 0],
+                         [0, 1, 0, 1, 1],
+                         [0, 0, 1, 1, 1],
+                         [0, 1, 1, 1, 0],
+                         [0, 1, 1, 1, 1],
+                         [0, 1, 1, 1, 1],
+                         [0, 0, 0, 0, 0]], dtype)
+        out = np.zeros(data.shape, bool)
+        ndimage.binary_hit_or_miss(data, struct, output=out)
+        assert_array_almost_equal(expected, out)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_hit_or_miss02(self, dtype):
+        struct = [[0, 1, 0],
+                  [1, 1, 1],
+                  [0, 1, 0]]
+        expected = [[0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 1, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 1, 0, 0, 1, 1, 1, 0],
+                         [1, 1, 1, 0, 0, 1, 0, 0],
+                         [0, 1, 0, 1, 1, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_hit_or_miss(data, struct)
+        assert_array_almost_equal(expected, out)
+
+    @pytest.mark.parametrize('dtype', types)
+    def test_hit_or_miss03(self, dtype):
+        struct1 = [[0, 0, 0],
+                   [1, 1, 1],
+                   [0, 0, 0]]
+        struct2 = [[1, 1, 1],
+                   [0, 0, 0],
+                   [1, 1, 1]]
+        expected = [[0, 0, 0, 0, 0, 1, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0],
+                    [0, 0, 1, 0, 0, 0, 0, 0],
+                    [0, 0, 0, 0, 0, 0, 0, 0]]
+        data = np.array([[0, 1, 0, 0, 1, 1, 1, 0],
+                         [1, 1, 1, 0, 0, 0, 0, 0],
+                         [0, 1, 0, 1, 1, 1, 1, 0],
+                         [0, 0, 1, 1, 1, 1, 1, 0],
+                         [0, 1, 1, 1, 0, 1, 1, 0],
+                         [0, 0, 0, 0, 1, 1, 1, 0],
+                         [0, 1, 1, 1, 1, 1, 1, 0],
+                         [0, 0, 0, 0, 0, 0, 0, 0]], dtype)
+        out = ndimage.binary_hit_or_miss(data, struct1, struct2)
+        assert_array_almost_equal(expected, out)
+
+
+class TestDilateFix:
+
+    def setup_method(self):
+        # dilation related setup
+        self.array = np.array([[0, 0, 0, 0, 0],
+                               [0, 0, 0, 0, 0],
+                               [0, 0, 0, 1, 0],
+                               [0, 0, 1, 1, 0],
+                               [0, 0, 0, 0, 0]], dtype=np.uint8)
+
+        self.sq3x3 = np.ones((3, 3))
+        dilated3x3 = ndimage.binary_dilation(self.array, structure=self.sq3x3)
+        self.dilated3x3 = dilated3x3.view(np.uint8)
+
+    def test_dilation_square_structure(self):
+        result = ndimage.grey_dilation(self.array, structure=self.sq3x3)
+        # +1 accounts for difference between grey and binary dilation
+        assert_array_almost_equal(result, self.dilated3x3 + 1)
+
+    def test_dilation_scalar_size(self):
+        result = ndimage.grey_dilation(self.array, size=3)
+        assert_array_almost_equal(result, self.dilated3x3)
+
+
+class TestBinaryOpeningClosing:
+
+    def setup_method(self):
+        a = np.zeros((5, 5), dtype=bool)
+        a[1:4, 1:4] = True
+        a[4, 4] = True
+        self.array = a
+        self.sq3x3 = np.ones((3, 3))
+        self.opened_old = ndimage.binary_opening(self.array, self.sq3x3,
+                                                 1, None, 0)
+        self.closed_old = ndimage.binary_closing(self.array, self.sq3x3,
+                                                 1, None, 0)
+
+    def test_opening_new_arguments(self):
+        opened_new = ndimage.binary_opening(self.array, self.sq3x3, 1, None,
+                                            0, None, 0, False)
+        assert_array_equal(opened_new, self.opened_old)
+
+    def test_closing_new_arguments(self):
+        closed_new = ndimage.binary_closing(self.array, self.sq3x3, 1, None,
+                                            0, None, 0, False)
+        assert_array_equal(closed_new, self.closed_old)
+
+
+def test_binary_erosion_noninteger_iterations():
+    # regression test for gh-9905, gh-9909: ValueError for
+    # non integer iterations
+    data = np.ones([1])
+    assert_raises(TypeError, ndimage.binary_erosion, data, iterations=0.5)
+    assert_raises(TypeError, ndimage.binary_erosion, data, iterations=1.5)
+
+
+def test_binary_dilation_noninteger_iterations():
+    # regression test for gh-9905, gh-9909: ValueError for
+    # non integer iterations
+    data = np.ones([1])
+    assert_raises(TypeError, ndimage.binary_dilation, data, iterations=0.5)
+    assert_raises(TypeError, ndimage.binary_dilation, data, iterations=1.5)
+
+
+def test_binary_opening_noninteger_iterations():
+    # regression test for gh-9905, gh-9909: ValueError for
+    # non integer iterations
+    data = np.ones([1])
+    assert_raises(TypeError, ndimage.binary_opening, data, iterations=0.5)
+    assert_raises(TypeError, ndimage.binary_opening, data, iterations=1.5)
+
+
+def test_binary_closing_noninteger_iterations():
+    # regression test for gh-9905, gh-9909: ValueError for
+    # non integer iterations
+    data = np.ones([1])
+    assert_raises(TypeError, ndimage.binary_closing, data, iterations=0.5)
+    assert_raises(TypeError, ndimage.binary_closing, data, iterations=1.5)
+
+
+def test_binary_closing_noninteger_brute_force_passes_when_true():
+    # regression test for gh-9905, gh-9909: ValueError for
+    # non integer iterations
+    data = np.ones([1])
+
+    assert ndimage.binary_erosion(
+        data, iterations=2, brute_force=1.5
+    ) == ndimage.binary_erosion(data, iterations=2, brute_force=bool(1.5))
+    assert ndimage.binary_erosion(
+        data, iterations=2, brute_force=0.0
+    ) == ndimage.binary_erosion(data, iterations=2, brute_force=bool(0.0))
+
+
+@pytest.mark.parametrize(
+    'function',
+    ['binary_erosion', 'binary_dilation', 'binary_opening', 'binary_closing'],
+)
+@pytest.mark.parametrize('iterations', [1, 5])
+@pytest.mark.parametrize('brute_force', [False, True])
+def test_binary_input_as_output(function, iterations, brute_force):
+    rstate = np.random.RandomState(123)
+    data = rstate.randint(low=0, high=2, size=100).astype(bool)
+    ndi_func = getattr(ndimage, function)
+
+    # input data is not modified
+    data_orig = data.copy()
+    expected = ndi_func(data, brute_force=brute_force, iterations=iterations)
+    assert_array_equal(data, data_orig)
+
+    # data should now contain the expected result
+    ndi_func(data, brute_force=brute_force, iterations=iterations, output=data)
+    assert_array_equal(expected, data)
+
+
+def test_binary_hit_or_miss_input_as_output():
+    rstate = np.random.RandomState(123)
+    data = rstate.randint(low=0, high=2, size=100).astype(bool)
+
+    # input data is not modified
+    data_orig = data.copy()
+    expected = ndimage.binary_hit_or_miss(data)
+    assert_array_equal(data, data_orig)
+
+    # data should now contain the expected result
+    ndimage.binary_hit_or_miss(data, output=data)
+    assert_array_equal(expected, data)
+
+
+def test_distance_transform_cdt_invalid_metric():
+    msg = 'invalid metric provided'
+    with pytest.raises(ValueError, match=msg):
+        ndimage.distance_transform_cdt(np.ones((5, 5)),
+                                       metric="garbage")
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_ni_support.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_ni_support.py
new file mode 100644
index 0000000000000000000000000000000000000000..a25429eebc8b3739e00465b43fd28ba24b320b45
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_ni_support.py
@@ -0,0 +1,77 @@
+import pytest
+
+import numpy as np
+from .._ni_support import _get_output
+
+
+@pytest.mark.parametrize(
+    'dtype',
+    [
+        # String specifiers
+        'f4', 'float32', 'complex64', 'complex128',
+        # Type and dtype specifiers
+        np.float32, float, np.dtype('f4'),
+        # Derive from input
+        None,
+    ],
+)
+def test_get_output_basic(dtype):
+    shape = (2, 3)
+
+    input_ = np.zeros(shape, 'float32')
+
+    # For None, derive dtype from input
+    expected_dtype = 'float32' if dtype is None else dtype
+
+    # Output is dtype-specifier, retrieve shape from input
+    result = _get_output(dtype, input_)
+    assert result.shape == shape
+    assert result.dtype == np.dtype(expected_dtype)
+
+    # Output is dtype specifier, with explicit shape, overriding input
+    result = _get_output(dtype, input_, shape=(3, 2))
+    assert result.shape == (3, 2)
+    assert result.dtype == np.dtype(expected_dtype)
+
+    # Output is pre-allocated array, return directly
+    output = np.zeros(shape, dtype)
+    result = _get_output(output, input_)
+    assert result is output
+
+
+def test_get_output_complex():
+    shape = (2, 3)
+
+    input_ = np.zeros(shape)
+
+    # None, promote input type to complex
+    result = _get_output(None, input_, complex_output=True)
+    assert result.shape == shape
+    assert result.dtype == np.dtype('complex128')
+
+    # Explicit type, promote type to complex
+    with pytest.warns(UserWarning, match='promoting specified output dtype to complex'):
+        result = _get_output(float, input_, complex_output=True)
+    assert result.shape == shape
+    assert result.dtype == np.dtype('complex128')
+
+    # String specifier, simply verify complex output
+    result = _get_output('complex64', input_, complex_output=True)
+    assert result.shape == shape
+    assert result.dtype == np.dtype('complex64')
+
+
+def test_get_output_error_cases():
+    input_ = np.zeros((2, 3), 'float32')
+
+    # Two separate paths can raise the same error
+    with pytest.raises(RuntimeError, match='output must have complex dtype'):
+        _get_output('float32', input_, complex_output=True)
+    with pytest.raises(RuntimeError, match='output must have complex dtype'):
+        _get_output(np.zeros((2, 3)), input_, complex_output=True)
+
+    with pytest.raises(RuntimeError, match='output must have numeric dtype'):
+        _get_output('void', input_)
+
+    with pytest.raises(RuntimeError, match='shape not correct'):
+        _get_output(np.zeros((3, 2)), input_)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_splines.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_splines.py
new file mode 100644
index 0000000000000000000000000000000000000000..a74e55111f8fac906f58a947db4a214da82a3cae
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/ndimage/tests/test_splines.py
@@ -0,0 +1,65 @@
+"""Tests for spline filtering."""
+import numpy as np
+import pytest
+
+from numpy.testing import assert_almost_equal
+
+from scipy import ndimage
+
+
+def get_spline_knot_values(order):
+    """Knot values to the right of a B-spline's center."""
+    knot_values = {0: [1],
+                   1: [1],
+                   2: [6, 1],
+                   3: [4, 1],
+                   4: [230, 76, 1],
+                   5: [66, 26, 1]}
+
+    return knot_values[order]
+
+
+def make_spline_knot_matrix(n, order, mode='mirror'):
+    """Matrix to invert to find the spline coefficients."""
+    knot_values = get_spline_knot_values(order)
+
+    matrix = np.zeros((n, n))
+    for diag, knot_value in enumerate(knot_values):
+        indices = np.arange(diag, n)
+        if diag == 0:
+            matrix[indices, indices] = knot_value
+        else:
+            matrix[indices, indices - diag] = knot_value
+            matrix[indices - diag, indices] = knot_value
+
+    knot_values_sum = knot_values[0] + 2 * sum(knot_values[1:])
+
+    if mode == 'mirror':
+        start, step = 1, 1
+    elif mode == 'reflect':
+        start, step = 0, 1
+    elif mode == 'grid-wrap':
+        start, step = -1, -1
+    else:
+        raise ValueError(f'unsupported mode {mode}')
+
+    for row in range(len(knot_values) - 1):
+        for idx, knot_value in enumerate(knot_values[row + 1:]):
+            matrix[row, start + step*idx] += knot_value
+            matrix[-row - 1, -start - 1 - step*idx] += knot_value
+
+    return matrix / knot_values_sum
+
+
+@pytest.mark.parametrize('order', [0, 1, 2, 3, 4, 5])
+@pytest.mark.parametrize('mode', ['mirror', 'grid-wrap', 'reflect'])
+def test_spline_filter_vs_matrix_solution(order, mode):
+    n = 100
+    eye = np.eye(n, dtype=float)
+    spline_filter_axis_0 = ndimage.spline_filter1d(eye, axis=0, order=order,
+                                                   mode=mode)
+    spline_filter_axis_1 = ndimage.spline_filter1d(eye, axis=1, order=order,
+                                                   mode=mode)
+    matrix = make_spline_knot_matrix(n, order, mode=mode)
+    assert_almost_equal(eye, np.dot(spline_filter_axis_0, matrix))
+    assert_almost_equal(eye, np.dot(spline_filter_axis_1, matrix.T))
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/__init__.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7854ecf302d5641c673849e33f4574593d92faff
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/__init__.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_arraytools.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_arraytools.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d0000eeccf245bfebd1679aaa15d692a1e8ec334
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_arraytools.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_bsplines.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_bsplines.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ecfb647be98ae8b30e6d9781b180017cd7e396b6
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_bsplines.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_czt.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_czt.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ddcc69651e9aa85023099f95238ac95f7dfcb7b3
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_czt.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_fir_filter_design.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_fir_filter_design.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..dc25652f321eed70b84907acac1151f0b2653bed
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_fir_filter_design.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_lti_conversion.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_lti_conversion.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..393636bac6446c2ea016834cf2f245b6e8c450ac
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_lti_conversion.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_ltisys.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_ltisys.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..6c849cc1f9f3c47ee8dddf0e2bffdf7bccdff653
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_ltisys.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_max_len_seq.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_max_len_seq.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a9fd42319068bae431c1bb993f2cf8379b10b401
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_max_len_seq.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_peak_finding.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_peak_finding.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..770a75ab9698b63bbcf5f86b3241da9ce8ad64c1
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_peak_finding.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_savitzky_golay.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_savitzky_golay.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3de9dc8dbcbc097176f503a4e7b3e949c98a64dc
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_savitzky_golay.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_short_time_fft.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_short_time_fft.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..be49d783eec0017cf78e287378c3efaca1b84feb
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_short_time_fft.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_spectral_py.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_spectral_py.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a63a90b48b4f095a2eb167defa6a510c0fea1278
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_spectral_py.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_upfirdn.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_upfirdn.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d8bf4e0c9727392a9fe67a5cd91a3a5f187b7280
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_upfirdn.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_waveforms.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_waveforms.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..45213becd846882b2dd978a4c6935fa2a0d61e85
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_waveforms.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_wavelets.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_wavelets.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..df00f0d44116a838105d9cc35b29126491e894a7
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/_wavelets.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/bsplines.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/bsplines.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8ba6c9fda4e398e4669bcf299ba9b9947240ca6e
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/bsplines.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/filter_design.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/filter_design.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..cc8e3dcea1c0b3b490fe27bd8d5fc55f22f00aa0
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/filter_design.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/fir_filter_design.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/fir_filter_design.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1781985ee7c8d37cf8e957dcf7f2a1fa74c8d8b3
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/fir_filter_design.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/lti_conversion.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/lti_conversion.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..573b1e64a0c683018bb344d6050f90ded5431c5e
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/lti_conversion.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/ltisys.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/ltisys.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f868043933e5e262c59505abd972fe12cbdad113
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/ltisys.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/signaltools.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/signaltools.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..598fce7e240f2a9f2023272bb784ecd0c5b6c652
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/signaltools.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/spectral.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/spectral.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..6c74479ee01090856e755623d644d058618032ec
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/spectral.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/spline.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/spline.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7d316d8bdac1a6beb359e62f3286cf0bba51fbbc
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/spline.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/waveforms.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/waveforms.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..84d84016c840d0f14fdacb36ec7d549e0de16b63
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/waveforms.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/wavelets.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/wavelets.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..00ae598bd6859da197842bb023753a9a85fa1998
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/__pycache__/wavelets.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__init__.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/__init__.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..13197b2f0c40deb66aaafc8006d10faaefd6d8db
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/__init__.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/_scipy_spectral_test_shim.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/_scipy_spectral_test_shim.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0c2174387e5fdfb4c900fa7b9e8e39aceac7a3c2
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/_scipy_spectral_test_shim.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/mpsig.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/mpsig.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9051d71b3e98fa9558b2e3fcd076cc1d6f212471
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/mpsig.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_array_tools.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_array_tools.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4640cd57aa4b4b82496d84c0a0e4f5ffd698ff09
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_array_tools.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_bsplines.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_bsplines.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5466a5ca2b0525e37edc102221427b6139610030
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_bsplines.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_cont2discrete.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_cont2discrete.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..397a5961414c5544dcd2fe910afa7449cb5ab180
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_cont2discrete.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_czt.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_czt.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..fbb1dd13edae085c86c5bb9bcb75c91b790c0a37
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_czt.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_dltisys.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_dltisys.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4cfa22427a759328ea3fd5d86d21ef2f6ce8ba2f
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_dltisys.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_fir_filter_design.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_fir_filter_design.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..497f99567ea7cceb1f38965ad05bdc945713c639
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_fir_filter_design.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_ltisys.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_ltisys.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..fb677ac5f99cb007ccaf8849171f28ac0d14ce77
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_ltisys.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_max_len_seq.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_max_len_seq.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..835844ffba1d913fbb0776ec5980059443035574
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_max_len_seq.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_peak_finding.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_peak_finding.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8aec460ebe00a99de380cf4c76bf151f27af8af5
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_peak_finding.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_result_type.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_result_type.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..161e1c3190dec1b8d7f9f69d07bca21b96e98b0a
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_result_type.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_savitzky_golay.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_savitzky_golay.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0922449274a945a930dcf57357b0bd6fe22ccc9f
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_savitzky_golay.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_short_time_fft.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_short_time_fft.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..312c494c292d95593753e2950554b46a19dc1286
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_short_time_fft.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_spectral.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_spectral.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b6cf70ca4ab72f3309e4af54dc3d3d0b0d5c93b0
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_spectral.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_upfirdn.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_upfirdn.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0f69d2013ad10e7a6a214f1a12271a0935960153
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_upfirdn.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_waveforms.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_waveforms.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..466a33efa0206b27cfc7944ff449843b3cf8fc7d
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_waveforms.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_wavelets.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_wavelets.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7d35ebb3fb98537b33fe6b0a82888732bbe05a86
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_wavelets.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_windows.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_windows.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..064bd95ce85056aafa8aac487d8081b8016b9456
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/__pycache__/test_windows.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/_scipy_spectral_test_shim.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/_scipy_spectral_test_shim.py
new file mode 100644
index 0000000000000000000000000000000000000000..c23f310bcae4fa85558f7f07cddb25874a0ec7d1
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/_scipy_spectral_test_shim.py
@@ -0,0 +1,488 @@
+"""Helpers to utilize existing stft / istft tests for testing `ShortTimeFFT`.
+
+This module provides the functions stft_compare() and istft_compare(), which,
+compares the output between the existing (i)stft() and the shortTimeFFT based
+_(i)stft_wrapper() implementations in this module.
+
+For testing add the following imports to the file ``tests/test_spectral.py``::
+
+    from ._scipy_spectral_test_shim import stft_compare as stft
+    from ._scipy_spectral_test_shim import istft_compare as istft
+
+and remove the existing imports of stft and istft.
+
+The idea of these wrappers is not to provide a backward-compatible interface
+but to demonstrate that the ShortTimeFFT implementation is at least as capable
+as the existing one and delivers comparable results. Furthermore, the
+wrappers highlight the different philosophies of the implementations,
+especially in the border handling.
+"""
+import platform
+from typing import cast, Literal
+
+import numpy as np
+from numpy.testing import assert_allclose
+
+from scipy.signal import ShortTimeFFT
+from scipy.signal import csd, get_window, stft, istft
+from scipy.signal._arraytools import const_ext, even_ext, odd_ext, zero_ext
+from scipy.signal._short_time_fft import FFT_MODE_TYPE
+from scipy.signal._spectral_py import _spectral_helper, _triage_segments, \
+    _median_bias
+
+
+def _stft_wrapper(x, fs=1.0, window='hann', nperseg=256, noverlap=None,
+                  nfft=None, detrend=False, return_onesided=True,
+                  boundary='zeros', padded=True, axis=-1, scaling='spectrum'):
+    """Wrapper for the SciPy `stft()` function based on `ShortTimeFFT` for
+    unit testing.
+
+    Handling the boundary and padding is where `ShortTimeFFT` and `stft()`
+    differ in behavior. Parts of `_spectral_helper()` were copied to mimic
+    the` stft()` behavior.
+
+    This function is meant to be solely used by `stft_compare()`.
+    """
+    if scaling not in ('psd', 'spectrum'):  # same errors as in original stft:
+        raise ValueError(f"Parameter {scaling=} not in ['spectrum', 'psd']!")
+
+    # The following lines are taken from the original _spectral_helper():
+    boundary_funcs = {'even': even_ext,
+                      'odd': odd_ext,
+                      'constant': const_ext,
+                      'zeros': zero_ext,
+                      None: None}
+
+    if boundary not in boundary_funcs:
+        raise ValueError(f"Unknown boundary option '{boundary}', must be one" +
+                         f" of: {list(boundary_funcs.keys())}")
+    if x.size == 0:
+        return np.empty(x.shape), np.empty(x.shape), np.empty(x.shape)
+
+    if nperseg is not None:  # if specified by user
+        nperseg = int(nperseg)
+        if nperseg < 1:
+            raise ValueError('nperseg must be a positive integer')
+
+    # parse window; if array like, then set nperseg = win.shape
+    win, nperseg = _triage_segments(window, nperseg,
+                                    input_length=x.shape[axis])
+
+    if nfft is None:
+        nfft = nperseg
+    elif nfft < nperseg:
+        raise ValueError('nfft must be greater than or equal to nperseg.')
+    else:
+        nfft = int(nfft)
+
+    if noverlap is None:
+        noverlap = nperseg//2
+    else:
+        noverlap = int(noverlap)
+    if noverlap >= nperseg:
+        raise ValueError('noverlap must be less than nperseg.')
+    nstep = nperseg - noverlap
+    n = x.shape[axis]
+
+    # Padding occurs after boundary extension, so that the extended signal ends
+    # in zeros, instead of introducing an impulse at the end.
+    # I.e. if x = [..., 3, 2]
+    # extend then pad -> [..., 3, 2, 2, 3, 0, 0, 0]
+    # pad then extend -> [..., 3, 2, 0, 0, 0, 2, 3]
+
+    if boundary is not None:
+        ext_func = boundary_funcs[boundary]
+        # Extend by nperseg//2 in front and back:
+        x = ext_func(x, nperseg//2, axis=axis)
+
+    if padded:
+        # Pad to integer number of windowed segments
+        # I.e make x.shape[-1] = nperseg + (nseg-1)*nstep, with integer nseg
+        x = np.moveaxis(x, axis, -1)
+
+        # This is an edge case where shortTimeFFT returns one more time slice
+        # than the Scipy stft() shorten to remove last time slice:
+        if n % 2 == 1 and nperseg % 2 == 1 and noverlap % 2 == 1:
+            x = x[..., :axis - 1]
+
+        nadd = (-(x.shape[-1]-nperseg) % nstep) % nperseg
+        zeros_shape = list(x.shape[:-1]) + [nadd]
+        x = np.concatenate((x, np.zeros(zeros_shape)), axis=-1)
+        x = np.moveaxis(x, -1, axis)
+
+    #  ... end original _spectral_helper() code.
+    scale_to = {'spectrum': 'magnitude', 'psd': 'psd'}[scaling]
+
+    if np.iscomplexobj(x) and return_onesided:
+        return_onesided = False
+    # using cast() to make mypy happy:
+    fft_mode = cast(FFT_MODE_TYPE, 'onesided' if return_onesided else 'twosided')
+
+    ST = ShortTimeFFT(win, nstep, fs, fft_mode=fft_mode, mfft=nfft,
+                      scale_to=scale_to, phase_shift=None)
+
+    k_off = nperseg // 2
+    p0 = 0  # ST.lower_border_end[1] + 1
+    nn = x.shape[axis] if padded else n+k_off+1
+    p1 = ST.upper_border_begin(nn)[1]  # ST.p_max(n) + 1
+
+    # This is bad hack to pass the test test_roundtrip_boundary_extension():
+    if padded is True and nperseg - noverlap == 1:
+        p1 -= nperseg // 2 - 1  # the reasoning behind this is not clear to me
+
+    detr = None if detrend is False else detrend
+    Sxx = ST.stft_detrend(x, detr, p0, p1, k_offset=k_off, axis=axis)
+    t = ST.t(nn, 0, p1 - p0, k_offset=0 if boundary is not None else k_off)
+    if x.dtype in (np.float32, np.complex64):
+        Sxx = Sxx.astype(np.complex64)
+
+    # workaround for test_average_all_segments() - seems to be buggy behavior:
+    if boundary is None and padded is False:
+        t, Sxx = t[1:-1], Sxx[..., :-2]
+        t -= k_off / fs
+
+    return ST.f, t, Sxx
+
+
+def _istft_wrapper(Zxx, fs=1.0, window='hann', nperseg=None, noverlap=None,
+                   nfft=None, input_onesided=True, boundary=True, time_axis=-1,
+                   freq_axis=-2, scaling='spectrum') -> \
+        tuple[np.ndarray, np.ndarray, tuple[int, int]]:
+    """Wrapper for the SciPy `istft()` function based on `ShortTimeFFT` for
+        unit testing.
+
+    Note that only option handling is implemented as far as to handle the unit
+    tests. E.g., the case ``nperseg=None`` is not handled.
+
+    This function is meant to be solely used by `istft_compare()`.
+    """
+    # *** Lines are taken from _spectral_py.istft() ***:
+    if Zxx.ndim < 2:
+        raise ValueError('Input stft must be at least 2d!')
+
+    if freq_axis == time_axis:
+        raise ValueError('Must specify differing time and frequency axes!')
+
+    nseg = Zxx.shape[time_axis]
+
+    if input_onesided:
+        # Assume even segment length
+        n_default = 2*(Zxx.shape[freq_axis] - 1)
+    else:
+        n_default = Zxx.shape[freq_axis]
+
+    # Check windowing parameters
+    if nperseg is None:
+        nperseg = n_default
+    else:
+        nperseg = int(nperseg)
+        if nperseg < 1:
+            raise ValueError('nperseg must be a positive integer')
+
+    if nfft is None:
+        if input_onesided and (nperseg == n_default + 1):
+            # Odd nperseg, no FFT padding
+            nfft = nperseg
+        else:
+            nfft = n_default
+    elif nfft < nperseg:
+        raise ValueError('nfft must be greater than or equal to nperseg.')
+    else:
+        nfft = int(nfft)
+
+    if noverlap is None:
+        noverlap = nperseg//2
+    else:
+        noverlap = int(noverlap)
+    if noverlap >= nperseg:
+        raise ValueError('noverlap must be less than nperseg.')
+    nstep = nperseg - noverlap
+
+    # Get window as array
+    if isinstance(window, str) or type(window) is tuple:
+        win = get_window(window, nperseg)
+    else:
+        win = np.asarray(window)
+        if len(win.shape) != 1:
+            raise ValueError('window must be 1-D')
+        if win.shape[0] != nperseg:
+            raise ValueError(f'window must have length of {nperseg}')
+
+    outputlength = nperseg + (nseg-1)*nstep
+    # *** End block of: Taken from _spectral_py.istft() ***
+
+    # Using cast() to make mypy happy:
+    fft_mode = cast(FFT_MODE_TYPE, 'onesided' if input_onesided else 'twosided')
+    scale_to = cast(Literal['magnitude', 'psd'],
+                    {'spectrum': 'magnitude', 'psd': 'psd'}[scaling])
+
+    ST = ShortTimeFFT(win, nstep, fs, fft_mode=fft_mode, mfft=nfft,
+                      scale_to=scale_to, phase_shift=None)
+
+    if boundary:
+        j = nperseg if nperseg % 2 == 0 else nperseg - 1
+        k0 = ST.k_min + nperseg // 2
+        k1 = outputlength - j + k0
+    else:
+        raise NotImplementedError("boundary=False does not make sense with" +
+                                  "ShortTimeFFT.istft()!")
+
+    x = ST.istft(Zxx, k0=k0, k1=k1, f_axis=freq_axis, t_axis=time_axis)
+    t = np.arange(k1 - k0) * ST.T
+    k_hi = ST.upper_border_begin(k1 - k0)[0]
+    # using cast() to make mypy happy:
+    return t, x, (ST.lower_border_end[0], k_hi)
+
+
+def _csd_wrapper(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None,
+                 nfft=None, detrend='constant', return_onesided=True,
+                 scaling='density', axis=-1, average='mean'):
+    """Wrapper for the `csd()` function based on `ShortTimeFFT` for
+        unit testing.
+    """
+    freqs, _, Pxy = _csd_test_shim(x, y, fs, window, nperseg, noverlap, nfft,
+                                   detrend, return_onesided, scaling, axis)
+
+    # The following code is taken from csd():
+    if len(Pxy.shape) >= 2 and Pxy.size > 0:
+        if Pxy.shape[-1] > 1:
+            if average == 'median':
+                # np.median must be passed real arrays for the desired result
+                bias = _median_bias(Pxy.shape[-1])
+                if np.iscomplexobj(Pxy):
+                    Pxy = (np.median(np.real(Pxy), axis=-1)
+                           + 1j * np.median(np.imag(Pxy), axis=-1))
+                else:
+                    Pxy = np.median(Pxy, axis=-1)
+                Pxy /= bias
+            elif average == 'mean':
+                Pxy = Pxy.mean(axis=-1)
+            else:
+                raise ValueError(f'average must be "median" or "mean", got {average}')
+        else:
+            Pxy = np.reshape(Pxy, Pxy.shape[:-1])
+
+    return freqs, Pxy
+
+
+def _csd_test_shim(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None,
+                   nfft=None, detrend='constant', return_onesided=True,
+                   scaling='density', axis=-1):
+    """Compare output of  _spectral_helper() and ShortTimeFFT, more
+    precisely _spect_helper_csd() for used in csd_wrapper().
+
+   The motivation of this function is to test if the ShortTimeFFT-based
+   wrapper `_spect_helper_csd()` returns the same values as `_spectral_helper`.
+   This function should only be usd by csd() in (unit) testing.
+   """
+    freqs, t, Pxy = _spectral_helper(x, y, fs, window, nperseg, noverlap, nfft,
+                                     detrend, return_onesided, scaling, axis,
+                                     mode='psd')
+    freqs1, Pxy1 = _spect_helper_csd(x, y, fs, window, nperseg, noverlap, nfft,
+                                     detrend, return_onesided, scaling, axis)
+
+    np.testing.assert_allclose(freqs1, freqs)
+    amax_Pxy = max(np.abs(Pxy).max(), 1) if Pxy.size else 1
+    atol = np.finfo(Pxy.dtype).resolution * amax_Pxy  # needed for large Pxy
+    # for c_ in range(Pxy.shape[-1]):
+    #    np.testing.assert_allclose(Pxy1[:, c_], Pxy[:, c_], atol=atol)
+    np.testing.assert_allclose(Pxy1, Pxy, atol=atol)
+    return freqs, t, Pxy
+
+
+def _spect_helper_csd(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None,
+                      nfft=None, detrend='constant', return_onesided=True,
+                      scaling='density', axis=-1):
+    """Wrapper for replacing _spectral_helper() by using the ShortTimeFFT
+      for use by csd().
+
+    This function should be only used by _csd_test_shim() and is only useful
+    for testing the ShortTimeFFT implementation.
+    """
+
+    # The following lines are taken from the original _spectral_helper():
+    same_data = y is x
+    axis = int(axis)
+
+    # Ensure we have np.arrays, get outdtype
+    x = np.asarray(x)
+    if not same_data:
+        y = np.asarray(y)
+    #     outdtype = np.result_type(x, y, np.complex64)
+    # else:
+    #     outdtype = np.result_type(x, np.complex64)
+
+    if not same_data:
+        # Check if we can broadcast the outer axes together
+        xouter = list(x.shape)
+        youter = list(y.shape)
+        xouter.pop(axis)
+        youter.pop(axis)
+        try:
+            outershape = np.broadcast(np.empty(xouter), np.empty(youter)).shape
+        except ValueError as e:
+            raise ValueError('x and y cannot be broadcast together.') from e
+
+    if same_data:
+        if x.size == 0:
+            return np.empty(x.shape), np.empty(x.shape)
+    else:
+        if x.size == 0 or y.size == 0:
+            outshape = outershape + (min([x.shape[axis], y.shape[axis]]),)
+            emptyout = np.moveaxis(np.empty(outshape), -1, axis)
+            return emptyout, emptyout
+
+    if nperseg is not None:  # if specified by user
+        nperseg = int(nperseg)
+        if nperseg < 1:
+            raise ValueError('nperseg must be a positive integer')
+
+    # parse window; if array like, then set nperseg = win.shape
+    n = x.shape[axis] if same_data else max(x.shape[axis], y.shape[axis])
+    win, nperseg = _triage_segments(window, nperseg, input_length=n)
+
+    if nfft is None:
+        nfft = nperseg
+    elif nfft < nperseg:
+        raise ValueError('nfft must be greater than or equal to nperseg.')
+    else:
+        nfft = int(nfft)
+
+    if noverlap is None:
+        noverlap = nperseg // 2
+    else:
+        noverlap = int(noverlap)
+    if noverlap >= nperseg:
+        raise ValueError('noverlap must be less than nperseg.')
+    nstep = nperseg - noverlap
+
+    if np.iscomplexobj(x) and return_onesided:
+        return_onesided = False
+
+    # using cast() to make mypy happy:
+    fft_mode = cast(FFT_MODE_TYPE, 'onesided' if return_onesided
+                    else 'twosided')
+    scale = {'spectrum': 'magnitude', 'density': 'psd'}[scaling]
+    SFT = ShortTimeFFT(win, nstep, fs, fft_mode=fft_mode, mfft=nfft,
+                       scale_to=scale, phase_shift=None)
+
+    # _spectral_helper() calculates X.conj()*Y instead of X*Y.conj():
+    Pxy = SFT.spectrogram(y, x, detr=None if detrend is False else detrend,
+                          p0=0, p1=(n-noverlap)//SFT.hop, k_offset=nperseg//2,
+                          axis=axis).conj()
+    # Note:
+    # 'onesided2X' scaling of ShortTimeFFT conflicts with the
+    # scaling='spectrum' parameter, since it doubles the squared magnitude,
+    # which in the view of the ShortTimeFFT implementation does not make sense.
+    # Hence, the doubling of the square is implemented here:
+    if return_onesided:
+        f_axis = Pxy.ndim - 1 + axis if axis < 0 else axis
+        Pxy = np.moveaxis(Pxy, f_axis, -1)
+        Pxy[..., 1:-1 if SFT.mfft % 2 == 0 else None] *= 2
+        Pxy = np.moveaxis(Pxy, -1, f_axis)
+
+    return SFT.f, Pxy
+
+
+def stft_compare(x, fs=1.0, window='hann', nperseg=256, noverlap=None,
+                 nfft=None, detrend=False, return_onesided=True,
+                 boundary='zeros', padded=True, axis=-1, scaling='spectrum'):
+    """Assert that the results from the existing `stft()` and `_stft_wrapper()`
+    are close to each other.
+
+    For comparing the STFT values an absolute tolerance of the floating point
+    resolution was added to circumvent problems with the following tests:
+    * For float32 the tolerances are much higher in
+      TestSTFT.test_roundtrip_float32()).
+    * The TestSTFT.test_roundtrip_scaling() has a high relative deviation.
+      Interestingly this did not appear in Scipy 1.9.1 but only in the current
+      development version.
+    """
+    kw = dict(x=x, fs=fs, window=window, nperseg=nperseg, noverlap=noverlap,
+              nfft=nfft, detrend=detrend, return_onesided=return_onesided,
+              boundary=boundary, padded=padded, axis=axis, scaling=scaling)
+    f, t, Zxx = stft(**kw)
+    f_wrapper, t_wrapper, Zxx_wrapper = _stft_wrapper(**kw)
+
+    e_msg_part = " of `stft_wrapper()` differ from `stft()`."
+    assert_allclose(f_wrapper, f, err_msg=f"Frequencies {e_msg_part}")
+    assert_allclose(t_wrapper, t, err_msg=f"Time slices {e_msg_part}")
+
+    # Adapted tolerances to account for:
+    atol = np.finfo(Zxx.dtype).resolution * 2
+    assert_allclose(Zxx_wrapper, Zxx, atol=atol,
+                    err_msg=f"STFT values {e_msg_part}")
+    return f, t, Zxx
+
+
+def istft_compare(Zxx, fs=1.0, window='hann', nperseg=None, noverlap=None,
+                  nfft=None, input_onesided=True, boundary=True, time_axis=-1,
+                  freq_axis=-2, scaling='spectrum'):
+    """Assert that the results from the existing `istft()` and
+    `_istft_wrapper()` are close to each other.
+
+    Quirks:
+    * If ``boundary=False`` the comparison is skipped, since it does not
+      make sense with ShortTimeFFT.istft(). Only used in test
+      TestSTFT.test_roundtrip_boundary_extension().
+    * If ShortTimeFFT.istft() decides the STFT is not invertible, the
+      comparison is skipped, since istft() only emits a warning and does not
+      return a correct result. Only used in
+      ShortTimeFFT.test_roundtrip_not_nola().
+    * For comparing the signals an absolute tolerance of the floating point
+      resolution was added to account for the low accuracy of float32 (Occurs
+      only in TestSTFT.test_roundtrip_float32()).
+    """
+    kw = dict(Zxx=Zxx, fs=fs, window=window, nperseg=nperseg,
+              noverlap=noverlap, nfft=nfft, input_onesided=input_onesided,
+              boundary=boundary, time_axis=time_axis, freq_axis=freq_axis,
+              scaling=scaling)
+
+    t, x = istft(**kw)
+    if not boundary:  # skip test_roundtrip_boundary_extension():
+        return t, x  # _istft_wrapper does() not implement this case
+    try:  # if inversion fails, istft() only emits a warning:
+        t_wrapper, x_wrapper, (k_lo, k_hi) = _istft_wrapper(**kw)
+    except ValueError as v:  # Do nothing if inversion fails:
+        if v.args[0] == "Short-time Fourier Transform not invertible!":
+            return t, x
+        raise v
+
+    e_msg_part = " of `istft_wrapper()` differ from `istft()`"
+    assert_allclose(t, t_wrapper, err_msg=f"Sample times {e_msg_part}")
+
+    # Adapted tolerances to account for resolution loss:
+    atol = np.finfo(x.dtype).resolution*2  # instead of default atol = 0
+    rtol = 1e-7  # default for np.allclose()
+
+    # Relax atol on 32-Bit platforms a bit to pass CI tests.
+    #  - Not clear why there are discrepancies (in the FFT maybe?)
+    #  - Not sure what changed on 'i686' since earlier on those test passed
+    if x.dtype == np.float32 and platform.machine() == 'i686':
+        # float32 gets only used by TestSTFT.test_roundtrip_float32() so
+        # we are using the tolerances from there to circumvent CI problems
+        atol, rtol = 1e-4, 1e-5
+    elif platform.machine() in ('aarch64', 'i386', 'i686'):
+        atol = max(atol, 1e-12)  # 2e-15 seems too tight for 32-Bit platforms
+
+    assert_allclose(x_wrapper[k_lo:k_hi], x[k_lo:k_hi], atol=atol, rtol=rtol,
+                    err_msg=f"Signal values {e_msg_part}")
+    return t, x
+
+
+def csd_compare(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None,
+                nfft=None, detrend='constant', return_onesided=True,
+                scaling='density', axis=-1, average='mean'):
+    """Assert that the results from the existing `csd()` and `_csd_wrapper()`
+    are close to each other. """
+    kw = dict(x=x, y=y, fs=fs, window=window, nperseg=nperseg,
+              noverlap=noverlap, nfft=nfft, detrend=detrend,
+              return_onesided=return_onesided, scaling=scaling, axis=axis,
+              average=average)
+    freqs0, Pxy0 = csd(**kw)
+    freqs1, Pxy1 = _csd_wrapper(**kw)
+
+    assert_allclose(freqs1, freqs0)
+    assert_allclose(Pxy1, Pxy0)
+    assert_allclose(freqs1, freqs0)
+    return freqs0, Pxy0
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/mpsig.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/mpsig.py
new file mode 100644
index 0000000000000000000000000000000000000000..d129de74e5df00c22bc0b82c7d3f7b52483941f9
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/mpsig.py
@@ -0,0 +1,122 @@
+"""
+Some signal functions implemented using mpmath.
+"""
+
+try:
+    import mpmath
+except ImportError:
+    mpmath = None
+
+
+def _prod(seq):
+    """Returns the product of the elements in the sequence `seq`."""
+    p = 1
+    for elem in seq:
+        p *= elem
+    return p
+
+
+def _relative_degree(z, p):
+    """
+    Return relative degree of transfer function from zeros and poles.
+
+    This is simply len(p) - len(z), which must be nonnegative.
+    A ValueError is raised if len(p) < len(z).
+    """
+    degree = len(p) - len(z)
+    if degree < 0:
+        raise ValueError("Improper transfer function. "
+                         "Must have at least as many poles as zeros.")
+    return degree
+
+
+def _zpkbilinear(z, p, k, fs):
+    """Bilinear transformation to convert a filter from analog to digital."""
+
+    degree = _relative_degree(z, p)
+
+    fs2 = 2*fs
+
+    # Bilinear transform the poles and zeros
+    z_z = [(fs2 + z1) / (fs2 - z1) for z1 in z]
+    p_z = [(fs2 + p1) / (fs2 - p1) for p1 in p]
+
+    # Any zeros that were at infinity get moved to the Nyquist frequency
+    z_z.extend([-1] * degree)
+
+    # Compensate for gain change
+    numer = _prod(fs2 - z1 for z1 in z)
+    denom = _prod(fs2 - p1 for p1 in p)
+    k_z = k * numer / denom
+
+    return z_z, p_z, k_z.real
+
+
+def _zpklp2lp(z, p, k, wo=1):
+    """Transform a lowpass filter to a different cutoff frequency."""
+
+    degree = _relative_degree(z, p)
+
+    # Scale all points radially from origin to shift cutoff frequency
+    z_lp = [wo * z1 for z1 in z]
+    p_lp = [wo * p1 for p1 in p]
+
+    # Each shifted pole decreases gain by wo, each shifted zero increases it.
+    # Cancel out the net change to keep overall gain the same
+    k_lp = k * wo**degree
+
+    return z_lp, p_lp, k_lp
+
+
+def _butter_analog_poles(n):
+    """
+    Poles of an analog Butterworth lowpass filter.
+
+    This is the same calculation as scipy.signal.buttap(n) or
+    scipy.signal.butter(n, 1, analog=True, output='zpk'), but mpmath is used,
+    and only the poles are returned.
+    """
+    poles = [-mpmath.exp(1j*mpmath.pi*k/(2*n)) for k in range(-n+1, n, 2)]
+    return poles
+
+
+def butter_lp(n, Wn):
+    """
+    Lowpass Butterworth digital filter design.
+
+    This computes the same result as scipy.signal.butter(n, Wn, output='zpk'),
+    but it uses mpmath, and the results are returned in lists instead of NumPy
+    arrays.
+    """
+    zeros = []
+    poles = _butter_analog_poles(n)
+    k = 1
+    fs = 2
+    warped = 2 * fs * mpmath.tan(mpmath.pi * Wn / fs)
+    z, p, k = _zpklp2lp(zeros, poles, k, wo=warped)
+    z, p, k = _zpkbilinear(z, p, k, fs=fs)
+    return z, p, k
+
+
+def zpkfreqz(z, p, k, worN=None):
+    """
+    Frequency response of a filter in zpk format, using mpmath.
+
+    This is the same calculation as scipy.signal.freqz, but the input is in
+    zpk format, the calculation is performed using mpath, and the results are
+    returned in lists instead of NumPy arrays.
+    """
+    if worN is None or isinstance(worN, int):
+        N = worN or 512
+        ws = [mpmath.pi * mpmath.mpf(j) / N for j in range(N)]
+    else:
+        ws = worN
+
+    h = []
+    for wk in ws:
+        zm1 = mpmath.exp(1j * wk)
+        numer = _prod([zm1 - t for t in z])
+        denom = _prod([zm1 - t for t in p])
+        hk = k * numer / denom
+        h.append(hk)
+    return ws, h
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_array_tools.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_array_tools.py
new file mode 100644
index 0000000000000000000000000000000000000000..81503b7e267cf9f74999d283b0d33b012fd0f77c
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_array_tools.py
@@ -0,0 +1,111 @@
+import numpy as np
+
+from numpy.testing import assert_array_equal
+from pytest import raises as assert_raises
+
+from scipy.signal._arraytools import (axis_slice, axis_reverse,
+     odd_ext, even_ext, const_ext, zero_ext)
+
+
+class TestArrayTools:
+
+    def test_axis_slice(self):
+        a = np.arange(12).reshape(3, 4)
+
+        s = axis_slice(a, start=0, stop=1, axis=0)
+        assert_array_equal(s, a[0:1, :])
+
+        s = axis_slice(a, start=-1, axis=0)
+        assert_array_equal(s, a[-1:, :])
+
+        s = axis_slice(a, start=0, stop=1, axis=1)
+        assert_array_equal(s, a[:, 0:1])
+
+        s = axis_slice(a, start=-1, axis=1)
+        assert_array_equal(s, a[:, -1:])
+
+        s = axis_slice(a, start=0, step=2, axis=0)
+        assert_array_equal(s, a[::2, :])
+
+        s = axis_slice(a, start=0, step=2, axis=1)
+        assert_array_equal(s, a[:, ::2])
+
+    def test_axis_reverse(self):
+        a = np.arange(12).reshape(3, 4)
+
+        r = axis_reverse(a, axis=0)
+        assert_array_equal(r, a[::-1, :])
+
+        r = axis_reverse(a, axis=1)
+        assert_array_equal(r, a[:, ::-1])
+
+    def test_odd_ext(self):
+        a = np.array([[1, 2, 3, 4, 5],
+                      [9, 8, 7, 6, 5]])
+
+        odd = odd_ext(a, 2, axis=1)
+        expected = np.array([[-1, 0, 1, 2, 3, 4, 5, 6, 7],
+                             [11, 10, 9, 8, 7, 6, 5, 4, 3]])
+        assert_array_equal(odd, expected)
+
+        odd = odd_ext(a, 1, axis=0)
+        expected = np.array([[-7, -4, -1, 2, 5],
+                             [1, 2, 3, 4, 5],
+                             [9, 8, 7, 6, 5],
+                             [17, 14, 11, 8, 5]])
+        assert_array_equal(odd, expected)
+
+        assert_raises(ValueError, odd_ext, a, 2, axis=0)
+        assert_raises(ValueError, odd_ext, a, 5, axis=1)
+
+    def test_even_ext(self):
+        a = np.array([[1, 2, 3, 4, 5],
+                      [9, 8, 7, 6, 5]])
+
+        even = even_ext(a, 2, axis=1)
+        expected = np.array([[3, 2, 1, 2, 3, 4, 5, 4, 3],
+                             [7, 8, 9, 8, 7, 6, 5, 6, 7]])
+        assert_array_equal(even, expected)
+
+        even = even_ext(a, 1, axis=0)
+        expected = np.array([[9, 8, 7, 6, 5],
+                             [1, 2, 3, 4, 5],
+                             [9, 8, 7, 6, 5],
+                             [1, 2, 3, 4, 5]])
+        assert_array_equal(even, expected)
+
+        assert_raises(ValueError, even_ext, a, 2, axis=0)
+        assert_raises(ValueError, even_ext, a, 5, axis=1)
+
+    def test_const_ext(self):
+        a = np.array([[1, 2, 3, 4, 5],
+                      [9, 8, 7, 6, 5]])
+
+        const = const_ext(a, 2, axis=1)
+        expected = np.array([[1, 1, 1, 2, 3, 4, 5, 5, 5],
+                             [9, 9, 9, 8, 7, 6, 5, 5, 5]])
+        assert_array_equal(const, expected)
+
+        const = const_ext(a, 1, axis=0)
+        expected = np.array([[1, 2, 3, 4, 5],
+                             [1, 2, 3, 4, 5],
+                             [9, 8, 7, 6, 5],
+                             [9, 8, 7, 6, 5]])
+        assert_array_equal(const, expected)
+
+    def test_zero_ext(self):
+        a = np.array([[1, 2, 3, 4, 5],
+                      [9, 8, 7, 6, 5]])
+
+        zero = zero_ext(a, 2, axis=1)
+        expected = np.array([[0, 0, 1, 2, 3, 4, 5, 0, 0],
+                             [0, 0, 9, 8, 7, 6, 5, 0, 0]])
+        assert_array_equal(zero, expected)
+
+        zero = zero_ext(a, 1, axis=0)
+        expected = np.array([[0, 0, 0, 0, 0],
+                             [1, 2, 3, 4, 5],
+                             [9, 8, 7, 6, 5],
+                             [0, 0, 0, 0, 0]])
+        assert_array_equal(zero, expected)
+
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_cont2discrete.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_cont2discrete.py
new file mode 100644
index 0000000000000000000000000000000000000000..51b9be56e29c4e0448020c2d8e8ff6e7e336f8c3
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_cont2discrete.py
@@ -0,0 +1,416 @@
+import numpy as np
+from numpy.testing import \
+                          assert_array_almost_equal, assert_almost_equal, \
+                          assert_allclose, assert_equal
+
+import pytest
+from scipy.signal import cont2discrete as c2d
+from scipy.signal import dlsim, ss2tf, ss2zpk, lsim, lti
+from scipy.signal import tf2ss, impulse, dimpulse, step, dstep
+
+# Author: Jeffrey Armstrong 
+# March 29, 2011
+
+
+class TestC2D:
+    def test_zoh(self):
+        ac = np.eye(2)
+        bc = np.full((2, 1), 0.5)
+        cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
+        dc = np.array([[0.0], [0.0], [-0.33]])
+
+        ad_truth = 1.648721270700128 * np.eye(2)
+        bd_truth = np.full((2, 1), 0.324360635350064)
+        # c and d in discrete should be equal to their continuous counterparts
+        dt_requested = 0.5
+
+        ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested, method='zoh')
+
+        assert_array_almost_equal(ad_truth, ad)
+        assert_array_almost_equal(bd_truth, bd)
+        assert_array_almost_equal(cc, cd)
+        assert_array_almost_equal(dc, dd)
+        assert_almost_equal(dt_requested, dt)
+
+    def test_foh(self):
+        ac = np.eye(2)
+        bc = np.full((2, 1), 0.5)
+        cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
+        dc = np.array([[0.0], [0.0], [-0.33]])
+
+        # True values are verified with Matlab
+        ad_truth = 1.648721270700128 * np.eye(2)
+        bd_truth = np.full((2, 1), 0.420839287058789)
+        cd_truth = cc
+        dd_truth = np.array([[0.260262223725224],
+                             [0.297442541400256],
+                             [-0.144098411624840]])
+        dt_requested = 0.5
+
+        ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested, method='foh')
+
+        assert_array_almost_equal(ad_truth, ad)
+        assert_array_almost_equal(bd_truth, bd)
+        assert_array_almost_equal(cd_truth, cd)
+        assert_array_almost_equal(dd_truth, dd)
+        assert_almost_equal(dt_requested, dt)
+
+    def test_impulse(self):
+        ac = np.eye(2)
+        bc = np.full((2, 1), 0.5)
+        cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
+        dc = np.array([[0.0], [0.0], [0.0]])
+
+        # True values are verified with Matlab
+        ad_truth = 1.648721270700128 * np.eye(2)
+        bd_truth = np.full((2, 1), 0.412180317675032)
+        cd_truth = cc
+        dd_truth = np.array([[0.4375], [0.5], [0.3125]])
+        dt_requested = 0.5
+
+        ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
+                                 method='impulse')
+
+        assert_array_almost_equal(ad_truth, ad)
+        assert_array_almost_equal(bd_truth, bd)
+        assert_array_almost_equal(cd_truth, cd)
+        assert_array_almost_equal(dd_truth, dd)
+        assert_almost_equal(dt_requested, dt)
+
+    def test_gbt(self):
+        ac = np.eye(2)
+        bc = np.full((2, 1), 0.5)
+        cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
+        dc = np.array([[0.0], [0.0], [-0.33]])
+
+        dt_requested = 0.5
+        alpha = 1.0 / 3.0
+
+        ad_truth = 1.6 * np.eye(2)
+        bd_truth = np.full((2, 1), 0.3)
+        cd_truth = np.array([[0.9, 1.2],
+                             [1.2, 1.2],
+                             [1.2, 0.3]])
+        dd_truth = np.array([[0.175],
+                             [0.2],
+                             [-0.205]])
+
+        ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
+                                 method='gbt', alpha=alpha)
+
+        assert_array_almost_equal(ad_truth, ad)
+        assert_array_almost_equal(bd_truth, bd)
+        assert_array_almost_equal(cd_truth, cd)
+        assert_array_almost_equal(dd_truth, dd)
+
+    def test_euler(self):
+        ac = np.eye(2)
+        bc = np.full((2, 1), 0.5)
+        cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
+        dc = np.array([[0.0], [0.0], [-0.33]])
+
+        dt_requested = 0.5
+
+        ad_truth = 1.5 * np.eye(2)
+        bd_truth = np.full((2, 1), 0.25)
+        cd_truth = np.array([[0.75, 1.0],
+                             [1.0, 1.0],
+                             [1.0, 0.25]])
+        dd_truth = dc
+
+        ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
+                                 method='euler')
+
+        assert_array_almost_equal(ad_truth, ad)
+        assert_array_almost_equal(bd_truth, bd)
+        assert_array_almost_equal(cd_truth, cd)
+        assert_array_almost_equal(dd_truth, dd)
+        assert_almost_equal(dt_requested, dt)
+
+    def test_backward_diff(self):
+        ac = np.eye(2)
+        bc = np.full((2, 1), 0.5)
+        cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
+        dc = np.array([[0.0], [0.0], [-0.33]])
+
+        dt_requested = 0.5
+
+        ad_truth = 2.0 * np.eye(2)
+        bd_truth = np.full((2, 1), 0.5)
+        cd_truth = np.array([[1.5, 2.0],
+                             [2.0, 2.0],
+                             [2.0, 0.5]])
+        dd_truth = np.array([[0.875],
+                             [1.0],
+                             [0.295]])
+
+        ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
+                                 method='backward_diff')
+
+        assert_array_almost_equal(ad_truth, ad)
+        assert_array_almost_equal(bd_truth, bd)
+        assert_array_almost_equal(cd_truth, cd)
+        assert_array_almost_equal(dd_truth, dd)
+
+    def test_bilinear(self):
+        ac = np.eye(2)
+        bc = np.full((2, 1), 0.5)
+        cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
+        dc = np.array([[0.0], [0.0], [-0.33]])
+
+        dt_requested = 0.5
+
+        ad_truth = (5.0 / 3.0) * np.eye(2)
+        bd_truth = np.full((2, 1), 1.0 / 3.0)
+        cd_truth = np.array([[1.0, 4.0 / 3.0],
+                             [4.0 / 3.0, 4.0 / 3.0],
+                             [4.0 / 3.0, 1.0 / 3.0]])
+        dd_truth = np.array([[0.291666666666667],
+                             [1.0 / 3.0],
+                             [-0.121666666666667]])
+
+        ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
+                                 method='bilinear')
+
+        assert_array_almost_equal(ad_truth, ad)
+        assert_array_almost_equal(bd_truth, bd)
+        assert_array_almost_equal(cd_truth, cd)
+        assert_array_almost_equal(dd_truth, dd)
+        assert_almost_equal(dt_requested, dt)
+
+        # Same continuous system again, but change sampling rate
+
+        ad_truth = 1.4 * np.eye(2)
+        bd_truth = np.full((2, 1), 0.2)
+        cd_truth = np.array([[0.9, 1.2], [1.2, 1.2], [1.2, 0.3]])
+        dd_truth = np.array([[0.175], [0.2], [-0.205]])
+
+        dt_requested = 1.0 / 3.0
+
+        ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
+                                 method='bilinear')
+
+        assert_array_almost_equal(ad_truth, ad)
+        assert_array_almost_equal(bd_truth, bd)
+        assert_array_almost_equal(cd_truth, cd)
+        assert_array_almost_equal(dd_truth, dd)
+        assert_almost_equal(dt_requested, dt)
+
+    def test_transferfunction(self):
+        numc = np.array([0.25, 0.25, 0.5])
+        denc = np.array([0.75, 0.75, 1.0])
+
+        numd = np.array([[1.0 / 3.0, -0.427419169438754, 0.221654141101125]])
+        dend = np.array([1.0, -1.351394049721225, 0.606530659712634])
+
+        dt_requested = 0.5
+
+        num, den, dt = c2d((numc, denc), dt_requested, method='zoh')
+
+        assert_array_almost_equal(numd, num)
+        assert_array_almost_equal(dend, den)
+        assert_almost_equal(dt_requested, dt)
+
+    def test_zerospolesgain(self):
+        zeros_c = np.array([0.5, -0.5])
+        poles_c = np.array([1.j / np.sqrt(2), -1.j / np.sqrt(2)])
+        k_c = 1.0
+
+        zeros_d = [1.23371727305860, 0.735356894461267]
+        polls_d = [0.938148335039729 + 0.346233593780536j,
+                   0.938148335039729 - 0.346233593780536j]
+        k_d = 1.0
+
+        dt_requested = 0.5
+
+        zeros, poles, k, dt = c2d((zeros_c, poles_c, k_c), dt_requested,
+                                  method='zoh')
+
+        assert_array_almost_equal(zeros_d, zeros)
+        assert_array_almost_equal(polls_d, poles)
+        assert_almost_equal(k_d, k)
+        assert_almost_equal(dt_requested, dt)
+
+    def test_gbt_with_sio_tf_and_zpk(self):
+        """Test method='gbt' with alpha=0.25 for tf and zpk cases."""
+        # State space coefficients for the continuous SIO system.
+        A = -1.0
+        B = 1.0
+        C = 1.0
+        D = 0.5
+
+        # The continuous transfer function coefficients.
+        cnum, cden = ss2tf(A, B, C, D)
+
+        # Continuous zpk representation
+        cz, cp, ck = ss2zpk(A, B, C, D)
+
+        h = 1.0
+        alpha = 0.25
+
+        # Explicit formulas, in the scalar case.
+        Ad = (1 + (1 - alpha) * h * A) / (1 - alpha * h * A)
+        Bd = h * B / (1 - alpha * h * A)
+        Cd = C / (1 - alpha * h * A)
+        Dd = D + alpha * C * Bd
+
+        # Convert the explicit solution to tf
+        dnum, dden = ss2tf(Ad, Bd, Cd, Dd)
+
+        # Compute the discrete tf using cont2discrete.
+        c2dnum, c2dden, dt = c2d((cnum, cden), h, method='gbt', alpha=alpha)
+
+        assert_allclose(dnum, c2dnum)
+        assert_allclose(dden, c2dden)
+
+        # Convert explicit solution to zpk.
+        dz, dp, dk = ss2zpk(Ad, Bd, Cd, Dd)
+
+        # Compute the discrete zpk using cont2discrete.
+        c2dz, c2dp, c2dk, dt = c2d((cz, cp, ck), h, method='gbt', alpha=alpha)
+
+        assert_allclose(dz, c2dz)
+        assert_allclose(dp, c2dp)
+        assert_allclose(dk, c2dk)
+
+    def test_discrete_approx(self):
+        """
+        Test that the solution to the discrete approximation of a continuous
+        system actually approximates the solution to the continuous system.
+        This is an indirect test of the correctness of the implementation
+        of cont2discrete.
+        """
+
+        def u(t):
+            return np.sin(2.5 * t)
+
+        a = np.array([[-0.01]])
+        b = np.array([[1.0]])
+        c = np.array([[1.0]])
+        d = np.array([[0.2]])
+        x0 = 1.0
+
+        t = np.linspace(0, 10.0, 101)
+        dt = t[1] - t[0]
+        u1 = u(t)
+
+        # Use lsim to compute the solution to the continuous system.
+        t, yout, xout = lsim((a, b, c, d), T=t, U=u1, X0=x0)
+
+        # Convert the continuous system to a discrete approximation.
+        dsys = c2d((a, b, c, d), dt, method='bilinear')
+
+        # Use dlsim with the pairwise averaged input to compute the output
+        # of the discrete system.
+        u2 = 0.5 * (u1[:-1] + u1[1:])
+        t2 = t[:-1]
+        td2, yd2, xd2 = dlsim(dsys, u=u2.reshape(-1, 1), t=t2, x0=x0)
+
+        # ymid is the average of consecutive terms of the "exact" output
+        # computed by lsim2.  This is what the discrete approximation
+        # actually approximates.
+        ymid = 0.5 * (yout[:-1] + yout[1:])
+
+        assert_allclose(yd2.ravel(), ymid, rtol=1e-4)
+
+    def test_simo_tf(self):
+        # See gh-5753
+        tf = ([[1, 0], [1, 1]], [1, 1])
+        num, den, dt = c2d(tf, 0.01)
+
+        assert_equal(dt, 0.01)  # sanity check
+        assert_allclose(den, [1, -0.990404983], rtol=1e-3)
+        assert_allclose(num, [[1, -1], [1, -0.99004983]], rtol=1e-3)
+
+    def test_multioutput(self):
+        ts = 0.01  # time step
+
+        tf = ([[1, -3], [1, 5]], [1, 1])
+        num, den, dt = c2d(tf, ts)
+
+        tf1 = (tf[0][0], tf[1])
+        num1, den1, dt1 = c2d(tf1, ts)
+
+        tf2 = (tf[0][1], tf[1])
+        num2, den2, dt2 = c2d(tf2, ts)
+
+        # Sanity checks
+        assert_equal(dt, dt1)
+        assert_equal(dt, dt2)
+
+        # Check that we get the same results
+        assert_allclose(num, np.vstack((num1, num2)), rtol=1e-13)
+
+        # Single input, so the denominator should
+        # not be multidimensional like the numerator
+        assert_allclose(den, den1, rtol=1e-13)
+        assert_allclose(den, den2, rtol=1e-13)
+
+class TestC2dLti:
+    def test_c2d_ss(self):
+        # StateSpace
+        A = np.array([[-0.3, 0.1], [0.2, -0.7]])
+        B = np.array([[0], [1]])
+        C = np.array([[1, 0]])
+        D = 0
+
+        A_res = np.array([[0.985136404135682, 0.004876671474795],
+                          [0.009753342949590, 0.965629718236502]])
+        B_res = np.array([[0.000122937599964], [0.049135527547844]])
+
+        sys_ssc = lti(A, B, C, D)
+        sys_ssd = sys_ssc.to_discrete(0.05)
+
+        assert_allclose(sys_ssd.A, A_res)
+        assert_allclose(sys_ssd.B, B_res)
+        assert_allclose(sys_ssd.C, C)
+        assert_allclose(sys_ssd.D, D)
+
+    def test_c2d_tf(self):
+
+        sys = lti([0.5, 0.3], [1.0, 0.4])
+        sys = sys.to_discrete(0.005)
+
+        # Matlab results
+        num_res = np.array([0.5, -0.485149004980066])
+        den_res = np.array([1.0, -0.980198673306755])
+
+        # Somehow a lot of numerical errors
+        assert_allclose(sys.den, den_res, atol=0.02)
+        assert_allclose(sys.num, num_res, atol=0.02)
+
+
+class TestC2dInvariants:
+    # Some test cases for checking the invariances.
+    # Array of triplets: (system, sample time, number of samples)
+    cases = [
+        (tf2ss([1, 1], [1, 1.5, 1]), 0.25, 10),
+        (tf2ss([1, 2], [1, 1.5, 3, 1]), 0.5, 10),
+        (tf2ss(0.1, [1, 1, 2, 1]), 0.5, 10),
+    ]
+
+    # Check that systems discretized with the impulse-invariant
+    # method really hold the invariant
+    @pytest.mark.parametrize("sys,sample_time,samples_number", cases)
+    def test_impulse_invariant(self, sys, sample_time, samples_number):
+        time = np.arange(samples_number) * sample_time
+        _, yout_cont = impulse(sys, T=time)
+        _, yout_disc = dimpulse(c2d(sys, sample_time, method='impulse'),
+                                n=len(time))
+        assert_allclose(sample_time * yout_cont.ravel(), yout_disc[0].ravel())
+
+    # Step invariant should hold for ZOH discretized systems
+    @pytest.mark.parametrize("sys,sample_time,samples_number", cases)
+    def test_step_invariant(self, sys, sample_time, samples_number):
+        time = np.arange(samples_number) * sample_time
+        _, yout_cont = step(sys, T=time)
+        _, yout_disc = dstep(c2d(sys, sample_time, method='zoh'), n=len(time))
+        assert_allclose(yout_cont.ravel(), yout_disc[0].ravel())
+
+    # Linear invariant should hold for FOH discretized systems
+    @pytest.mark.parametrize("sys,sample_time,samples_number", cases)
+    def test_linear_invariant(self, sys, sample_time, samples_number):
+        time = np.arange(samples_number) * sample_time
+        _, yout_cont, _ = lsim(sys, T=time, U=time)
+        _, yout_disc, _ = dlsim(c2d(sys, sample_time, method='foh'), u=time)
+        assert_allclose(yout_cont.ravel(), yout_disc.ravel())
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_czt.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_czt.py
new file mode 100644
index 0000000000000000000000000000000000000000..b4a3c0e37c83812f525f2565144fcf9b1d7eeffd
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_czt.py
@@ -0,0 +1,219 @@
+# This program is public domain
+# Authors: Paul Kienzle, Nadav Horesh
+'''
+A unit test module for czt.py
+'''
+import pytest
+from numpy.testing import assert_allclose
+from scipy.fft import fft
+from scipy.signal import (czt, zoom_fft, czt_points, CZT, ZoomFFT)
+import numpy as np
+
+
+def check_czt(x):
+    # Check that czt is the equivalent of normal fft
+    y = fft(x)
+    y1 = czt(x)
+    assert_allclose(y1, y, rtol=1e-13)
+
+    # Check that interpolated czt is the equivalent of normal fft
+    y = fft(x, 100*len(x))
+    y1 = czt(x, 100*len(x))
+    assert_allclose(y1, y, rtol=1e-12)
+
+
+def check_zoom_fft(x):
+    # Check that zoom_fft is the equivalent of normal fft
+    y = fft(x)
+    y1 = zoom_fft(x, [0, 2-2./len(y)], endpoint=True)
+    assert_allclose(y1, y, rtol=1e-11, atol=1e-14)
+    y1 = zoom_fft(x, [0, 2])
+    assert_allclose(y1, y, rtol=1e-11, atol=1e-14)
+
+    # Test fn scalar
+    y1 = zoom_fft(x, 2-2./len(y), endpoint=True)
+    assert_allclose(y1, y, rtol=1e-11, atol=1e-14)
+    y1 = zoom_fft(x, 2)
+    assert_allclose(y1, y, rtol=1e-11, atol=1e-14)
+
+    # Check that zoom_fft with oversampling is equivalent to zero padding
+    over = 10
+    yover = fft(x, over*len(x))
+    y2 = zoom_fft(x, [0, 2-2./len(yover)], m=len(yover), endpoint=True)
+    assert_allclose(y2, yover, rtol=1e-12, atol=1e-10)
+    y2 = zoom_fft(x, [0, 2], m=len(yover))
+    assert_allclose(y2, yover, rtol=1e-12, atol=1e-10)
+
+    # Check that zoom_fft works on a subrange
+    w = np.linspace(0, 2-2./len(x), len(x))
+    f1, f2 = w[3], w[6]
+    y3 = zoom_fft(x, [f1, f2], m=3*over+1, endpoint=True)
+    idx3 = slice(3*over, 6*over+1)
+    assert_allclose(y3, yover[idx3], rtol=1e-13)
+
+
+def test_1D():
+    # Test of 1D version of the transforms
+
+    np.random.seed(0)  # Deterministic randomness
+
+    # Random signals
+    lengths = np.random.randint(8, 200, 20)
+    np.append(lengths, 1)
+    for length in lengths:
+        x = np.random.random(length)
+        check_zoom_fft(x)
+        check_czt(x)
+
+    # Gauss
+    t = np.linspace(-2, 2, 128)
+    x = np.exp(-t**2/0.01)
+    check_zoom_fft(x)
+
+    # Linear
+    x = [1, 2, 3, 4, 5, 6, 7]
+    check_zoom_fft(x)
+
+    # Check near powers of two
+    check_zoom_fft(range(126-31))
+    check_zoom_fft(range(127-31))
+    check_zoom_fft(range(128-31))
+    check_zoom_fft(range(129-31))
+    check_zoom_fft(range(130-31))
+
+    # Check transform on n-D array input
+    x = np.reshape(np.arange(3*2*28), (3, 2, 28))
+    y1 = zoom_fft(x, [0, 2-2./28])
+    y2 = zoom_fft(x[2, 0, :], [0, 2-2./28])
+    assert_allclose(y1[2, 0], y2, rtol=1e-13, atol=1e-12)
+
+    y1 = zoom_fft(x, [0, 2], endpoint=False)
+    y2 = zoom_fft(x[2, 0, :], [0, 2], endpoint=False)
+    assert_allclose(y1[2, 0], y2, rtol=1e-13, atol=1e-12)
+
+    # Random (not a test condition)
+    x = np.random.rand(101)
+    check_zoom_fft(x)
+
+    # Spikes
+    t = np.linspace(0, 1, 128)
+    x = np.sin(2*np.pi*t*5)+np.sin(2*np.pi*t*13)
+    check_zoom_fft(x)
+
+    # Sines
+    x = np.zeros(100, dtype=complex)
+    x[[1, 5, 21]] = 1
+    check_zoom_fft(x)
+
+    # Sines plus complex component
+    x += 1j*np.linspace(0, 0.5, x.shape[0])
+    check_zoom_fft(x)
+
+
+def test_large_prime_lengths():
+    np.random.seed(0)  # Deterministic randomness
+    for N in (101, 1009, 10007):
+        x = np.random.rand(N)
+        y = fft(x)
+        y1 = czt(x)
+        assert_allclose(y, y1, rtol=1e-12)
+
+
+@pytest.mark.slow
+def test_czt_vs_fft():
+    np.random.seed(123)
+    random_lengths = np.random.exponential(100000, size=10).astype('int')
+    for n in random_lengths:
+        a = np.random.randn(n)
+        assert_allclose(czt(a), fft(a), rtol=1e-11)
+
+
+def test_empty_input():
+    with pytest.raises(ValueError, match='Invalid number of CZT'):
+        czt([])
+    with pytest.raises(ValueError, match='Invalid number of CZT'):
+        zoom_fft([], 0.5)
+
+
+def test_0_rank_input():
+    with pytest.raises(IndexError, match='tuple index out of range'):
+        czt(5)
+    with pytest.raises(IndexError, match='tuple index out of range'):
+        zoom_fft(5, 0.5)
+
+
+@pytest.mark.parametrize('impulse', ([0, 0, 1], [0, 0, 1, 0, 0],
+                                     np.concatenate((np.array([0, 0, 1]),
+                                                     np.zeros(100)))))
+@pytest.mark.parametrize('m', (1, 3, 5, 8, 101, 1021))
+@pytest.mark.parametrize('a', (1, 2, 0.5, 1.1))
+# Step that tests away from the unit circle, but not so far it explodes from
+# numerical error
+@pytest.mark.parametrize('w', (None, 0.98534 + 0.17055j))
+def test_czt_math(impulse, m, w, a):
+    # z-transform of an impulse is 1 everywhere
+    assert_allclose(czt(impulse[2:], m=m, w=w, a=a),
+                    np.ones(m), rtol=1e-10)
+
+    # z-transform of a delayed impulse is z**-1
+    assert_allclose(czt(impulse[1:], m=m, w=w, a=a),
+                    czt_points(m=m, w=w, a=a)**-1, rtol=1e-10)
+
+    # z-transform of a 2-delayed impulse is z**-2
+    assert_allclose(czt(impulse, m=m, w=w, a=a),
+                    czt_points(m=m, w=w, a=a)**-2, rtol=1e-10)
+
+
+def test_int_args():
+    # Integer argument `a` was producing all 0s
+    assert_allclose(abs(czt([0, 1], m=10, a=2)), 0.5*np.ones(10), rtol=1e-15)
+    assert_allclose(czt_points(11, w=2), 1/(2**np.arange(11)), rtol=1e-30)
+
+
+def test_czt_points():
+    for N in (1, 2, 3, 8, 11, 100, 101, 10007):
+        assert_allclose(czt_points(N), np.exp(2j*np.pi*np.arange(N)/N),
+                        rtol=1e-30)
+
+    assert_allclose(czt_points(7, w=1), np.ones(7), rtol=1e-30)
+    assert_allclose(czt_points(11, w=2.), 1/(2**np.arange(11)), rtol=1e-30)
+
+    func = CZT(12, m=11, w=2., a=1)
+    assert_allclose(func.points(), 1/(2**np.arange(11)), rtol=1e-30)
+
+
+@pytest.mark.parametrize('cls, args', [(CZT, (100,)), (ZoomFFT, (100, 0.2))])
+def test_CZT_size_mismatch(cls, args):
+    # Data size doesn't match function's expected size
+    myfunc = cls(*args)
+    with pytest.raises(ValueError, match='CZT defined for'):
+        myfunc(np.arange(5))
+
+
+def test_invalid_range():
+    with pytest.raises(ValueError, match='2-length sequence'):
+        ZoomFFT(100, [1, 2, 3])
+
+
+@pytest.mark.parametrize('m', [0, -11, 5.5, 4.0])
+def test_czt_points_errors(m):
+    # Invalid number of points
+    with pytest.raises(ValueError, match='Invalid number of CZT'):
+        czt_points(m)
+
+
+@pytest.mark.parametrize('size', [0, -5, 3.5, 4.0])
+def test_nonsense_size(size):
+    # Numpy and Scipy fft() give ValueError for 0 output size, so we do, too
+    with pytest.raises(ValueError, match='Invalid number of CZT'):
+        CZT(size, 3)
+    with pytest.raises(ValueError, match='Invalid number of CZT'):
+        ZoomFFT(size, 0.2, 3)
+    with pytest.raises(ValueError, match='Invalid number of CZT'):
+        CZT(3, size)
+    with pytest.raises(ValueError, match='Invalid number of CZT'):
+        ZoomFFT(3, 0.2, size)
+    with pytest.raises(ValueError, match='Invalid number of CZT'):
+        czt([1, 2, 3], size)
+    with pytest.raises(ValueError, match='Invalid number of CZT'):
+        zoom_fft([1, 2, 3], 0.2, size)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_filter_design.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_filter_design.py
new file mode 100644
index 0000000000000000000000000000000000000000..47d24323edbfa35788d9b6f548d6a8f288f14194
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_filter_design.py
@@ -0,0 +1,4442 @@
+import warnings
+
+from scipy._lib import _pep440
+import numpy as np
+from numpy.testing import (assert_array_almost_equal,
+                           assert_array_almost_equal_nulp,
+                           assert_array_equal, assert_array_less,
+                           assert_equal, assert_,
+                           assert_allclose, assert_warns, suppress_warnings)
+import pytest
+from pytest import raises as assert_raises
+
+from numpy import array, spacing, sin, pi, sort, sqrt
+from scipy.signal import (argrelextrema, BadCoefficients, bessel, besselap, bilinear,
+                          buttap, butter, buttord, cheb1ap, cheb1ord, cheb2ap,
+                          cheb2ord, cheby1, cheby2, ellip, ellipap, ellipord,
+                          firwin, freqs_zpk, freqs, freqz, freqz_zpk,
+                          gammatone, group_delay, iircomb, iirdesign, iirfilter,
+                          iirnotch, iirpeak, lp2bp, lp2bs, lp2hp, lp2lp, normalize,
+                          medfilt, order_filter,
+                          sos2tf, sos2zpk, sosfreqz, tf2sos, tf2zpk, zpk2sos,
+                          zpk2tf, bilinear_zpk, lp2lp_zpk, lp2hp_zpk, lp2bp_zpk,
+                          lp2bs_zpk)
+from scipy.signal._filter_design import (_cplxreal, _cplxpair, _norm_factor,
+                                        _bessel_poly, _bessel_zeros)
+
+try:
+    import mpmath
+except ImportError:
+    mpmath = None
+
+
+def mpmath_check(min_ver):
+    return pytest.mark.skipif(
+        mpmath is None
+        or _pep440.parse(mpmath.__version__) < _pep440.Version(min_ver),
+        reason=f"mpmath version >= {min_ver} required",
+    )
+
+
+class TestCplxPair:
+
+    def test_trivial_input(self):
+        assert_equal(_cplxpair([]).size, 0)
+        assert_equal(_cplxpair(1), 1)
+
+    def test_output_order(self):
+        assert_allclose(_cplxpair([1+1j, 1-1j]), [1-1j, 1+1j])
+
+        a = [1+1j, 1+1j, 1, 1-1j, 1-1j, 2]
+        b = [1-1j, 1+1j, 1-1j, 1+1j, 1, 2]
+        assert_allclose(_cplxpair(a), b)
+
+        # points spaced around the unit circle
+        z = np.exp(2j*pi*array([4, 3, 5, 2, 6, 1, 0])/7)
+        z1 = np.copy(z)
+        np.random.shuffle(z)
+        assert_allclose(_cplxpair(z), z1)
+        np.random.shuffle(z)
+        assert_allclose(_cplxpair(z), z1)
+        np.random.shuffle(z)
+        assert_allclose(_cplxpair(z), z1)
+
+        # Should be able to pair up all the conjugates
+        x = np.random.rand(10000) + 1j * np.random.rand(10000)
+        y = x.conj()
+        z = np.random.rand(10000)
+        x = np.concatenate((x, y, z))
+        np.random.shuffle(x)
+        c = _cplxpair(x)
+
+        # Every other element of head should be conjugates:
+        assert_allclose(c[0:20000:2], np.conj(c[1:20000:2]))
+        # Real parts of head should be in sorted order:
+        assert_allclose(c[0:20000:2].real, np.sort(c[0:20000:2].real))
+        # Tail should be sorted real numbers:
+        assert_allclose(c[20000:], np.sort(c[20000:]))
+
+    def test_real_integer_input(self):
+        assert_array_equal(_cplxpair([2, 0, 1]), [0, 1, 2])
+
+    def test_tolerances(self):
+        eps = spacing(1)
+        assert_allclose(_cplxpair([1j, -1j, 1+1j*eps], tol=2*eps),
+                        [-1j, 1j, 1+1j*eps])
+
+        # sorting close to 0
+        assert_allclose(_cplxpair([-eps+1j, +eps-1j]), [-1j, +1j])
+        assert_allclose(_cplxpair([+eps+1j, -eps-1j]), [-1j, +1j])
+        assert_allclose(_cplxpair([+1j, -1j]), [-1j, +1j])
+
+    def test_unmatched_conjugates(self):
+        # 1+2j is unmatched
+        assert_raises(ValueError, _cplxpair, [1+3j, 1-3j, 1+2j])
+
+        # 1+2j and 1-3j are unmatched
+        assert_raises(ValueError, _cplxpair, [1+3j, 1-3j, 1+2j, 1-3j])
+
+        # 1+3j is unmatched
+        assert_raises(ValueError, _cplxpair, [1+3j, 1-3j, 1+3j])
+
+        # Not conjugates
+        assert_raises(ValueError, _cplxpair, [4+5j, 4+5j])
+        assert_raises(ValueError, _cplxpair, [1-7j, 1-7j])
+
+        # No pairs
+        assert_raises(ValueError, _cplxpair, [1+3j])
+        assert_raises(ValueError, _cplxpair, [1-3j])
+
+
+class TestCplxReal:
+
+    def test_trivial_input(self):
+        assert_equal(_cplxreal([]), ([], []))
+        assert_equal(_cplxreal(1), ([], [1]))
+
+    def test_output_order(self):
+        zc, zr = _cplxreal(np.roots(array([1, 0, 0, 1])))
+        assert_allclose(np.append(zc, zr), [1/2 + 1j*sin(pi/3), -1])
+
+        eps = spacing(1)
+
+        a = [0+1j, 0-1j, eps + 1j, eps - 1j, -eps + 1j, -eps - 1j,
+             1, 4, 2, 3, 0, 0,
+             2+3j, 2-3j,
+             1-eps + 1j, 1+2j, 1-2j, 1+eps - 1j,  # sorts out of order
+             3+1j, 3+1j, 3+1j, 3-1j, 3-1j, 3-1j,
+             2-3j, 2+3j]
+        zc, zr = _cplxreal(a)
+        assert_allclose(zc, [1j, 1j, 1j, 1+1j, 1+2j, 2+3j, 2+3j, 3+1j, 3+1j,
+                             3+1j])
+        assert_allclose(zr, [0, 0, 1, 2, 3, 4])
+
+        z = array([1-eps + 1j, 1+2j, 1-2j, 1+eps - 1j, 1+eps+3j, 1-2*eps-3j,
+                   0+1j, 0-1j, 2+4j, 2-4j, 2+3j, 2-3j, 3+7j, 3-7j, 4-eps+1j,
+                   4+eps-2j, 4-1j, 4-eps+2j])
+
+        zc, zr = _cplxreal(z)
+        assert_allclose(zc, [1j, 1+1j, 1+2j, 1+3j, 2+3j, 2+4j, 3+7j, 4+1j,
+                             4+2j])
+        assert_equal(zr, [])
+
+    def test_unmatched_conjugates(self):
+        # 1+2j is unmatched
+        assert_raises(ValueError, _cplxreal, [1+3j, 1-3j, 1+2j])
+
+        # 1+2j and 1-3j are unmatched
+        assert_raises(ValueError, _cplxreal, [1+3j, 1-3j, 1+2j, 1-3j])
+
+        # 1+3j is unmatched
+        assert_raises(ValueError, _cplxreal, [1+3j, 1-3j, 1+3j])
+
+        # No pairs
+        assert_raises(ValueError, _cplxreal, [1+3j])
+        assert_raises(ValueError, _cplxreal, [1-3j])
+
+    def test_real_integer_input(self):
+        zc, zr = _cplxreal([2, 0, 1, 4])
+        assert_array_equal(zc, [])
+        assert_array_equal(zr, [0, 1, 2, 4])
+
+
+class TestTf2zpk:
+
+    @pytest.mark.parametrize('dt', (np.float64, np.complex128))
+    def test_simple(self, dt):
+        z_r = np.array([0.5, -0.5])
+        p_r = np.array([1.j / np.sqrt(2), -1.j / np.sqrt(2)])
+        # Sort the zeros/poles so that we don't fail the test if the order
+        # changes
+        z_r.sort()
+        p_r.sort()
+        b = np.poly(z_r).astype(dt)
+        a = np.poly(p_r).astype(dt)
+
+        z, p, k = tf2zpk(b, a)
+        z.sort()
+        # The real part of `p` is ~0.0, so sort by imaginary part
+        p = p[np.argsort(p.imag)]
+
+        assert_array_almost_equal(z, z_r)
+        assert_array_almost_equal(p, p_r)
+        assert_array_almost_equal(k, 1.)
+        assert k.dtype == dt
+
+    def test_bad_filter(self):
+        # Regression test for #651: better handling of badly conditioned
+        # filter coefficients.
+        with suppress_warnings():
+            warnings.simplefilter("error", BadCoefficients)
+            assert_raises(BadCoefficients, tf2zpk, [1e-15], [1.0, 1.0])
+
+
+class TestZpk2Tf:
+
+    def test_identity(self):
+        """Test the identity transfer function."""
+        z = []
+        p = []
+        k = 1.
+        b, a = zpk2tf(z, p, k)
+        b_r = np.array([1.])  # desired result
+        a_r = np.array([1.])  # desired result
+        # The test for the *type* of the return values is a regression
+        # test for ticket #1095. In the case p=[], zpk2tf used to
+        # return the scalar 1.0 instead of array([1.0]).
+        assert_array_equal(b, b_r)
+        assert_(isinstance(b, np.ndarray))
+        assert_array_equal(a, a_r)
+        assert_(isinstance(a, np.ndarray))
+
+
+class TestSos2Zpk:
+
+    def test_basic(self):
+        sos = [[1, 0, 1, 1, 0, -0.81],
+               [1, 0, 0, 1, 0, +0.49]]
+        z, p, k = sos2zpk(sos)
+        z2 = [1j, -1j, 0, 0]
+        p2 = [0.9, -0.9, 0.7j, -0.7j]
+        k2 = 1
+        assert_array_almost_equal(sort(z), sort(z2), decimal=4)
+        assert_array_almost_equal(sort(p), sort(p2), decimal=4)
+        assert_array_almost_equal(k, k2)
+
+        sos = [[1.00000, +0.61803, 1.0000, 1.00000, +0.60515, 0.95873],
+               [1.00000, -1.61803, 1.0000, 1.00000, -1.58430, 0.95873],
+               [1.00000, +1.00000, 0.0000, 1.00000, +0.97915, 0.00000]]
+        z, p, k = sos2zpk(sos)
+        z2 = [-0.3090 + 0.9511j, -0.3090 - 0.9511j, 0.8090 + 0.5878j,
+              0.8090 - 0.5878j, -1.0000 + 0.0000j, 0]
+        p2 = [-0.3026 + 0.9312j, -0.3026 - 0.9312j, 0.7922 + 0.5755j,
+              0.7922 - 0.5755j, -0.9791 + 0.0000j, 0]
+        k2 = 1
+        assert_array_almost_equal(sort(z), sort(z2), decimal=4)
+        assert_array_almost_equal(sort(p), sort(p2), decimal=4)
+
+        sos = array([[1, 2, 3, 1, 0.2, 0.3],
+                     [4, 5, 6, 1, 0.4, 0.5]])
+        z = array([-1 - 1.41421356237310j, -1 + 1.41421356237310j,
+                  -0.625 - 1.05326872164704j, -0.625 + 1.05326872164704j])
+        p = array([-0.2 - 0.678232998312527j, -0.2 + 0.678232998312527j,
+                  -0.1 - 0.538516480713450j, -0.1 + 0.538516480713450j])
+        k = 4
+        z2, p2, k2 = sos2zpk(sos)
+        assert_allclose(_cplxpair(z2), z)
+        assert_allclose(_cplxpair(p2), p)
+        assert_allclose(k2, k)
+
+    def test_fewer_zeros(self):
+        """Test not the expected number of p/z (effectively at origin)."""
+        sos = butter(3, 0.1, output='sos')
+        z, p, k = sos2zpk(sos)
+        assert len(z) == 4
+        assert len(p) == 4
+
+        sos = butter(12, [5., 30.], 'bandpass', fs=1200., analog=False,
+                    output='sos')
+        with pytest.warns(BadCoefficients, match='Badly conditioned'):
+            z, p, k = sos2zpk(sos)
+        assert len(z) == 24
+        assert len(p) == 24
+
+
+class TestSos2Tf:
+
+    def test_basic(self):
+        sos = [[1, 1, 1, 1, 0, -1],
+               [-2, 3, 1, 1, 10, 1]]
+        b, a = sos2tf(sos)
+        assert_array_almost_equal(b, [-2, 1, 2, 4, 1])
+        assert_array_almost_equal(a, [1, 10, 0, -10, -1])
+
+
+class TestTf2Sos:
+
+    def test_basic(self):
+        num = [2, 16, 44, 56, 32]
+        den = [3, 3, -15, 18, -12]
+        sos = tf2sos(num, den)
+        sos2 = [[0.6667, 4.0000, 5.3333, 1.0000, +2.0000, -4.0000],
+                [1.0000, 2.0000, 2.0000, 1.0000, -1.0000, +1.0000]]
+        assert_array_almost_equal(sos, sos2, decimal=4)
+
+        b = [1, -3, 11, -27, 18]
+        a = [16, 12, 2, -4, -1]
+        sos = tf2sos(b, a)
+        sos2 = [[0.0625, -0.1875, 0.1250, 1.0000, -0.2500, -0.1250],
+                [1.0000, +0.0000, 9.0000, 1.0000, +1.0000, +0.5000]]
+        # assert_array_almost_equal(sos, sos2, decimal=4)
+
+    @pytest.mark.parametrize('b, a, analog, sos',
+                             [([1], [1], False, [[1., 0., 0., 1., 0., 0.]]),
+                              ([1], [1], True, [[0., 0., 1., 0., 0., 1.]]),
+                              ([1], [1., 0., -1.01, 0, 0.01], False,
+                               [[1., 0., 0., 1., 0., -0.01],
+                                [1., 0., 0., 1., 0., -1]]),
+                              ([1], [1., 0., -1.01, 0, 0.01], True,
+                               [[0., 0., 1., 1., 0., -1],
+                                [0., 0., 1., 1., 0., -0.01]])])
+    def test_analog(self, b, a, analog, sos):
+        sos2 = tf2sos(b, a, analog=analog)
+        assert_array_almost_equal(sos, sos2, decimal=4)
+
+
+class TestZpk2Sos:
+
+    @pytest.mark.parametrize('dt', 'fdgFDG')
+    @pytest.mark.parametrize('pairing, analog',
+                             [('nearest', False),
+                              ('keep_odd', False),
+                              ('minimal', False),
+                              ('minimal', True)])
+    def test_dtypes(self, dt, pairing, analog):
+        z = np.array([-1, -1]).astype(dt)
+        ct = dt.upper()  # the poles have to be complex
+        p = np.array([0.57149 + 0.29360j, 0.57149 - 0.29360j]).astype(ct)
+        k = np.array(1).astype(dt)
+        sos = zpk2sos(z, p, k, pairing=pairing, analog=analog)
+        sos2 = [[1, 2, 1, 1, -1.14298, 0.41280]]  # octave & MATLAB
+        assert_array_almost_equal(sos, sos2, decimal=4)
+
+    def test_basic(self):
+        for pairing in ('nearest', 'keep_odd'):
+            #
+            # Cases that match octave
+            #
+
+            z = [-1, -1]
+            p = [0.57149 + 0.29360j, 0.57149 - 0.29360j]
+            k = 1
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            sos2 = [[1, 2, 1, 1, -1.14298, 0.41280]]  # octave & MATLAB
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            z = [1j, -1j]
+            p = [0.9, -0.9, 0.7j, -0.7j]
+            k = 1
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            sos2 = [[1, 0, 1, 1, 0, +0.49],
+                    [1, 0, 0, 1, 0, -0.81]]  # octave
+            # sos2 = [[0, 0, 1, 1, -0.9, 0],
+            #         [1, 0, 1, 1, 0.9, 0]]  # MATLAB
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            z = []
+            p = [0.8, -0.5+0.25j, -0.5-0.25j]
+            k = 1.
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            sos2 = [[1., 0., 0., 1., 1., 0.3125],
+                    [1., 0., 0., 1., -0.8, 0.]]  # octave, MATLAB fails
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            z = [1., 1., 0.9j, -0.9j]
+            p = [0.99+0.01j, 0.99-0.01j, 0.1+0.9j, 0.1-0.9j]
+            k = 1
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            sos2 = [[1, 0, 0.81, 1, -0.2, 0.82],
+                    [1, -2, 1, 1, -1.98, 0.9802]]  # octave
+            # sos2 = [[1, -2, 1, 1,  -0.2, 0.82],
+            #         [1, 0, 0.81, 1, -1.98, 0.9802]]  # MATLAB
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            z = [0.9+0.1j, 0.9-0.1j, -0.9]
+            p = [0.75+0.25j, 0.75-0.25j, 0.9]
+            k = 1
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            if pairing == 'keep_odd':
+                sos2 = [[1, -1.8, 0.82, 1, -1.5, 0.625],
+                        [1, 0.9, 0, 1, -0.9, 0]]  # octave; MATLAB fails
+                assert_array_almost_equal(sos, sos2, decimal=4)
+            else:  # pairing == 'nearest'
+                sos2 = [[1, 0.9, 0, 1, -1.5, 0.625],
+                        [1, -1.8, 0.82, 1, -0.9, 0]]  # our algorithm
+                assert_array_almost_equal(sos, sos2, decimal=4)
+
+            #
+            # Cases that differ from octave:
+            #
+
+            z = [-0.3090 + 0.9511j, -0.3090 - 0.9511j, 0.8090 + 0.5878j,
+                 +0.8090 - 0.5878j, -1.0000 + 0.0000j]
+            p = [-0.3026 + 0.9312j, -0.3026 - 0.9312j, 0.7922 + 0.5755j,
+                 +0.7922 - 0.5755j, -0.9791 + 0.0000j]
+            k = 1
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            # sos2 = [[1, 0.618, 1, 1, 0.6052, 0.95870],
+            #         [1, -1.618, 1, 1, -1.5844, 0.95878],
+            #         [1, 1, 0, 1, 0.9791, 0]]  # octave, MATLAB fails
+            sos2 = [[1, 1, 0, 1, +0.97915, 0],
+                    [1, 0.61803, 1, 1, +0.60515, 0.95873],
+                    [1, -1.61803, 1, 1, -1.58430, 0.95873]]
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            z = [-1 - 1.4142j, -1 + 1.4142j,
+                 -0.625 - 1.0533j, -0.625 + 1.0533j]
+            p = [-0.2 - 0.6782j, -0.2 + 0.6782j,
+                 -0.1 - 0.5385j, -0.1 + 0.5385j]
+            k = 4
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            sos2 = [[4, 8, 12, 1, 0.2, 0.3],
+                    [1, 1.25, 1.5, 1, 0.4, 0.5]]  # MATLAB
+            # sos2 = [[4, 8, 12, 1, 0.4, 0.5],
+            #         [1, 1.25, 1.5, 1, 0.2, 0.3]]  # octave
+            assert_allclose(sos, sos2, rtol=1e-4, atol=1e-4)
+
+            z = []
+            p = [0.2, -0.5+0.25j, -0.5-0.25j]
+            k = 1.
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            sos2 = [[1., 0., 0., 1., -0.2, 0.],
+                    [1., 0., 0., 1., 1., 0.3125]]
+            # sos2 = [[1., 0., 0., 1., 1., 0.3125],
+            #         [1., 0., 0., 1., -0.2, 0]]  # octave, MATLAB fails
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            # The next two examples are adapted from Leland B. Jackson,
+            # "Digital Filters and Signal Processing (1995) p.400:
+            # http://books.google.com/books?id=VZ8uabI1pNMC&lpg=PA400&ots=gRD9pi8Jua&dq=Pole%2Fzero%20pairing%20for%20minimum%20roundoff%20noise%20in%20BSF.&pg=PA400#v=onepage&q=Pole%2Fzero%20pairing%20for%20minimum%20roundoff%20noise%20in%20BSF.&f=false
+
+            deg2rad = np.pi / 180.
+            k = 1.
+
+            # first example
+            thetas = [22.5, 45, 77.5]
+            mags = [0.8, 0.6, 0.9]
+            z = np.array([np.exp(theta * deg2rad * 1j) for theta in thetas])
+            z = np.concatenate((z, np.conj(z)))
+            p = np.array([mag * np.exp(theta * deg2rad * 1j)
+                          for theta, mag in zip(thetas, mags)])
+            p = np.concatenate((p, np.conj(p)))
+            sos = zpk2sos(z, p, k)
+            # sos2 = [[1, -0.43288, 1, 1, -0.38959, 0.81],  # octave,
+            #         [1, -1.41421, 1, 1, -0.84853, 0.36],  # MATLAB fails
+            #         [1, -1.84776, 1, 1, -1.47821, 0.64]]
+            # Note that pole-zero pairing matches, but ordering is different
+            sos2 = [[1, -1.41421, 1, 1, -0.84853, 0.36],
+                    [1, -1.84776, 1, 1, -1.47821, 0.64],
+                    [1, -0.43288, 1, 1, -0.38959, 0.81]]
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            # second example
+            z = np.array([np.exp(theta * deg2rad * 1j)
+                          for theta in (85., 10.)])
+            z = np.concatenate((z, np.conj(z), [1, -1]))
+            sos = zpk2sos(z, p, k)
+
+            # sos2 = [[1, -0.17431, 1, 1, -0.38959, 0.81],  # octave "wrong",
+            #         [1, -1.96962, 1, 1, -0.84853, 0.36],  # MATLAB fails
+            #         [1, 0, -1, 1, -1.47821, 0.64000]]
+            # Our pole-zero pairing matches the text, Octave does not
+            sos2 = [[1, 0, -1, 1, -0.84853, 0.36],
+                    [1, -1.96962, 1, 1, -1.47821, 0.64],
+                    [1, -0.17431, 1, 1, -0.38959, 0.81]]
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+    # these examples are taken from the doc string, and show the
+    # effect of the 'pairing' argument
+    @pytest.mark.parametrize('pairing, sos',
+                             [('nearest',
+                               np.array([[1., 1., 0.5, 1., -0.75, 0.],
+                                         [1., 1., 0., 1., -1.6, 0.65]])),
+                              ('keep_odd',
+                               np.array([[1., 1., 0, 1., -0.75, 0.],
+                                         [1., 1., 0.5, 1., -1.6, 0.65]])),
+                              ('minimal',
+                               np.array([[0., 1., 1., 0., 1., -0.75],
+                                         [1., 1., 0.5, 1., -1.6, 0.65]]))])
+    def test_pairing(self, pairing, sos):
+        z1 = np.array([-1, -0.5-0.5j, -0.5+0.5j])
+        p1 = np.array([0.75, 0.8+0.1j, 0.8-0.1j])
+        sos2 = zpk2sos(z1, p1, 1, pairing=pairing)
+        assert_array_almost_equal(sos, sos2, decimal=4)
+
+    @pytest.mark.parametrize('p, sos_dt',
+                             [([-1, 1, -0.1, 0.1],
+                               [[0., 0., 1., 1., 0., -0.01],
+                                [0., 0., 1., 1., 0., -1]]),
+                              ([-0.7071+0.7071j, -0.7071-0.7071j, -0.1j, 0.1j],
+                               [[0., 0., 1., 1., 0., 0.01],
+                                [0., 0., 1., 1., 1.4142, 1.]])])
+    def test_analog(self, p, sos_dt):
+        # test `analog` argument
+        # for discrete time, poles closest to unit circle should appear last
+        # for cont. time, poles closest to imaginary axis should appear last
+        sos2_dt = zpk2sos([], p, 1, pairing='minimal', analog=False)
+        sos2_ct = zpk2sos([], p, 1, pairing='minimal', analog=True)
+        assert_array_almost_equal(sos_dt, sos2_dt, decimal=4)
+        assert_array_almost_equal(sos_dt[::-1], sos2_ct, decimal=4)
+
+    def test_bad_args(self):
+        with pytest.raises(ValueError, match=r'pairing must be one of'):
+            zpk2sos([1], [2], 1, pairing='no_such_pairing')
+
+        with pytest.raises(ValueError, match=r'.*pairing must be "minimal"'):
+            zpk2sos([1], [2], 1, pairing='keep_odd', analog=True)
+
+        with pytest.raises(ValueError,
+                           match=r'.*must have len\(p\)>=len\(z\)'):
+            zpk2sos([1, 1], [2], 1, analog=True)
+
+        with pytest.raises(ValueError, match=r'k must be real'):
+            zpk2sos([1], [2], k=1j)
+
+
+class TestFreqs:
+
+    def test_basic(self):
+        _, h = freqs([1.0], [1.0], worN=8)
+        assert_array_almost_equal(h, np.ones(8))
+
+    def test_output(self):
+        # 1st order low-pass filter: H(s) = 1 / (s + 1)
+        w = [0.1, 1, 10, 100]
+        num = [1]
+        den = [1, 1]
+        w, H = freqs(num, den, worN=w)
+        s = w * 1j
+        expected = 1 / (s + 1)
+        assert_array_almost_equal(H.real, expected.real)
+        assert_array_almost_equal(H.imag, expected.imag)
+
+    def test_freq_range(self):
+        # Test that freqresp() finds a reasonable frequency range.
+        # 1st order low-pass filter: H(s) = 1 / (s + 1)
+        # Expected range is from 0.01 to 10.
+        num = [1]
+        den = [1, 1]
+        n = 10
+        expected_w = np.logspace(-2, 1, n)
+        w, H = freqs(num, den, worN=n)
+        assert_array_almost_equal(w, expected_w)
+
+    def test_plot(self):
+
+        def plot(w, h):
+            assert_array_almost_equal(h, np.ones(8))
+
+        assert_raises(ZeroDivisionError, freqs, [1.0], [1.0], worN=8,
+                      plot=lambda w, h: 1 / 0)
+        freqs([1.0], [1.0], worN=8, plot=plot)
+
+    def test_backward_compat(self):
+        # For backward compatibility, test if None act as a wrapper for default
+        w1, h1 = freqs([1.0], [1.0])
+        w2, h2 = freqs([1.0], [1.0], None)
+        assert_array_almost_equal(w1, w2)
+        assert_array_almost_equal(h1, h2)
+
+    def test_w_or_N_types(self):
+        # Measure at 8 equally-spaced points
+        for N in (8, np.int8(8), np.int16(8), np.int32(8), np.int64(8),
+                  np.array(8)):
+            w, h = freqs([1.0], [1.0], worN=N)
+            assert_equal(len(w), 8)
+            assert_array_almost_equal(h, np.ones(8))
+
+        # Measure at frequency 8 rad/sec
+        for w in (8.0, 8.0+0j):
+            w_out, h = freqs([1.0], [1.0], worN=w)
+            assert_array_almost_equal(w_out, [8])
+            assert_array_almost_equal(h, [1])
+
+
+class TestFreqs_zpk:
+
+    def test_basic(self):
+        _, h = freqs_zpk([1.0], [1.0], [1.0], worN=8)
+        assert_array_almost_equal(h, np.ones(8))
+
+    def test_output(self):
+        # 1st order low-pass filter: H(s) = 1 / (s + 1)
+        w = [0.1, 1, 10, 100]
+        z = []
+        p = [-1]
+        k = 1
+        w, H = freqs_zpk(z, p, k, worN=w)
+        s = w * 1j
+        expected = 1 / (s + 1)
+        assert_array_almost_equal(H.real, expected.real)
+        assert_array_almost_equal(H.imag, expected.imag)
+
+    def test_freq_range(self):
+        # Test that freqresp() finds a reasonable frequency range.
+        # 1st order low-pass filter: H(s) = 1 / (s + 1)
+        # Expected range is from 0.01 to 10.
+        z = []
+        p = [-1]
+        k = 1
+        n = 10
+        expected_w = np.logspace(-2, 1, n)
+        w, H = freqs_zpk(z, p, k, worN=n)
+        assert_array_almost_equal(w, expected_w)
+
+    def test_vs_freqs(self):
+        b, a = cheby1(4, 5, 100, analog=True, output='ba')
+        z, p, k = cheby1(4, 5, 100, analog=True, output='zpk')
+
+        w1, h1 = freqs(b, a)
+        w2, h2 = freqs_zpk(z, p, k)
+        assert_allclose(w1, w2)
+        assert_allclose(h1, h2, rtol=1e-6)
+
+    def test_backward_compat(self):
+        # For backward compatibility, test if None act as a wrapper for default
+        w1, h1 = freqs_zpk([1.0], [1.0], [1.0])
+        w2, h2 = freqs_zpk([1.0], [1.0], [1.0], None)
+        assert_array_almost_equal(w1, w2)
+        assert_array_almost_equal(h1, h2)
+
+    def test_w_or_N_types(self):
+        # Measure at 8 equally-spaced points
+        for N in (8, np.int8(8), np.int16(8), np.int32(8), np.int64(8),
+                  np.array(8)):
+            w, h = freqs_zpk([], [], 1, worN=N)
+            assert_equal(len(w), 8)
+            assert_array_almost_equal(h, np.ones(8))
+
+        # Measure at frequency 8 rad/sec
+        for w in (8.0, 8.0+0j):
+            w_out, h = freqs_zpk([], [], 1, worN=w)
+            assert_array_almost_equal(w_out, [8])
+            assert_array_almost_equal(h, [1])
+
+
+class TestFreqz:
+
+    def test_ticket1441(self):
+        """Regression test for ticket 1441."""
+        # Because freqz previously used arange instead of linspace,
+        # when N was large, it would return one more point than
+        # requested.
+        N = 100000
+        w, h = freqz([1.0], worN=N)
+        assert_equal(w.shape, (N,))
+
+    def test_basic(self):
+        w, h = freqz([1.0], worN=8)
+        assert_array_almost_equal(w, np.pi * np.arange(8) / 8.)
+        assert_array_almost_equal(h, np.ones(8))
+        w, h = freqz([1.0], worN=9)
+        assert_array_almost_equal(w, np.pi * np.arange(9) / 9.)
+        assert_array_almost_equal(h, np.ones(9))
+
+        for a in [1, np.ones(2)]:
+            w, h = freqz(np.ones(2), a, worN=0)
+            assert_equal(w.shape, (0,))
+            assert_equal(h.shape, (0,))
+            assert_equal(h.dtype, np.dtype('complex128'))
+
+        t = np.linspace(0, 1, 4, endpoint=False)
+        for b, a, h_whole in zip(
+                ([1., 0, 0, 0], np.sin(2 * np.pi * t)),
+                ([1., 0, 0, 0], [0.5, 0, 0, 0]),
+                ([1., 1., 1., 1.], [0, -4j, 0, 4j])):
+            w, h = freqz(b, a, worN=4, whole=True)
+            expected_w = np.linspace(0, 2 * np.pi, 4, endpoint=False)
+            assert_array_almost_equal(w, expected_w)
+            assert_array_almost_equal(h, h_whole)
+            # simultaneously check int-like support
+            w, h = freqz(b, a, worN=np.int32(4), whole=True)
+            assert_array_almost_equal(w, expected_w)
+            assert_array_almost_equal(h, h_whole)
+            w, h = freqz(b, a, worN=w, whole=True)
+            assert_array_almost_equal(w, expected_w)
+            assert_array_almost_equal(h, h_whole)
+
+    def test_basic_whole(self):
+        w, h = freqz([1.0], worN=8, whole=True)
+        assert_array_almost_equal(w, 2 * np.pi * np.arange(8.0) / 8)
+        assert_array_almost_equal(h, np.ones(8))
+
+    def test_plot(self):
+
+        def plot(w, h):
+            assert_array_almost_equal(w, np.pi * np.arange(8.0) / 8)
+            assert_array_almost_equal(h, np.ones(8))
+
+        assert_raises(ZeroDivisionError, freqz, [1.0], worN=8,
+                      plot=lambda w, h: 1 / 0)
+        freqz([1.0], worN=8, plot=plot)
+
+    def test_fft_wrapping(self):
+        # Some simple real FIR filters
+        bs = list()  # filters
+        as_ = list()
+        hs_whole = list()
+        hs_half = list()
+        # 3 taps
+        t = np.linspace(0, 1, 3, endpoint=False)
+        bs.append(np.sin(2 * np.pi * t))
+        as_.append(3.)
+        hs_whole.append([0, -0.5j, 0.5j])
+        hs_half.append([0, np.sqrt(1./12.), -0.5j])
+        # 4 taps
+        t = np.linspace(0, 1, 4, endpoint=False)
+        bs.append(np.sin(2 * np.pi * t))
+        as_.append(0.5)
+        hs_whole.append([0, -4j, 0, 4j])
+        hs_half.append([0, np.sqrt(8), -4j, -np.sqrt(8)])
+        del t
+        for ii, b in enumerate(bs):
+            # whole
+            a = as_[ii]
+            expected_w = np.linspace(0, 2 * np.pi, len(b), endpoint=False)
+            w, h = freqz(b, a, worN=expected_w, whole=True)  # polyval
+            err_msg = f'b = {b}, a={a}'
+            assert_array_almost_equal(w, expected_w, err_msg=err_msg)
+            assert_array_almost_equal(h, hs_whole[ii], err_msg=err_msg)
+            w, h = freqz(b, a, worN=len(b), whole=True)  # FFT
+            assert_array_almost_equal(w, expected_w, err_msg=err_msg)
+            assert_array_almost_equal(h, hs_whole[ii], err_msg=err_msg)
+            # non-whole
+            expected_w = np.linspace(0, np.pi, len(b), endpoint=False)
+            w, h = freqz(b, a, worN=expected_w, whole=False)  # polyval
+            assert_array_almost_equal(w, expected_w, err_msg=err_msg)
+            assert_array_almost_equal(h, hs_half[ii], err_msg=err_msg)
+            w, h = freqz(b, a, worN=len(b), whole=False)  # FFT
+            assert_array_almost_equal(w, expected_w, err_msg=err_msg)
+            assert_array_almost_equal(h, hs_half[ii], err_msg=err_msg)
+
+        # some random FIR filters (real + complex)
+        # assume polyval is accurate
+        rng = np.random.RandomState(0)
+        for ii in range(2, 10):  # number of taps
+            b = rng.randn(ii)
+            for kk in range(2):
+                a = rng.randn(1) if kk == 0 else rng.randn(3)
+                for jj in range(2):
+                    if jj == 1:
+                        b = b + rng.randn(ii) * 1j
+                    # whole
+                    expected_w = np.linspace(0, 2 * np.pi, ii, endpoint=False)
+                    w, expected_h = freqz(b, a, worN=expected_w, whole=True)
+                    assert_array_almost_equal(w, expected_w)
+                    w, h = freqz(b, a, worN=ii, whole=True)
+                    assert_array_almost_equal(w, expected_w)
+                    assert_array_almost_equal(h, expected_h)
+                    # half
+                    expected_w = np.linspace(0, np.pi, ii, endpoint=False)
+                    w, expected_h = freqz(b, a, worN=expected_w, whole=False)
+                    assert_array_almost_equal(w, expected_w)
+                    w, h = freqz(b, a, worN=ii, whole=False)
+                    assert_array_almost_equal(w, expected_w)
+                    assert_array_almost_equal(h, expected_h)
+
+    def test_broadcasting1(self):
+        # Test broadcasting with worN an integer or a 1-D array,
+        # b and a are n-dimensional arrays.
+        np.random.seed(123)
+        b = np.random.rand(3, 5, 1)
+        a = np.random.rand(2, 1)
+        for whole in [False, True]:
+            # Test with worN being integers (one fast for FFT and one not),
+            # a 1-D array, and an empty array.
+            for worN in [16, 17, np.linspace(0, 1, 10), np.array([])]:
+                w, h = freqz(b, a, worN=worN, whole=whole)
+                for k in range(b.shape[1]):
+                    bk = b[:, k, 0]
+                    ak = a[:, 0]
+                    ww, hh = freqz(bk, ak, worN=worN, whole=whole)
+                    assert_allclose(ww, w)
+                    assert_allclose(hh, h[k])
+
+    def test_broadcasting2(self):
+        # Test broadcasting with worN an integer or a 1-D array,
+        # b is an n-dimensional array, and a is left at the default value.
+        np.random.seed(123)
+        b = np.random.rand(3, 5, 1)
+        for whole in [False, True]:
+            for worN in [16, 17, np.linspace(0, 1, 10)]:
+                w, h = freqz(b, worN=worN, whole=whole)
+                for k in range(b.shape[1]):
+                    bk = b[:, k, 0]
+                    ww, hh = freqz(bk, worN=worN, whole=whole)
+                    assert_allclose(ww, w)
+                    assert_allclose(hh, h[k])
+
+    def test_broadcasting3(self):
+        # Test broadcasting where b.shape[-1] is the same length
+        # as worN, and a is left at the default value.
+        np.random.seed(123)
+        N = 16
+        b = np.random.rand(3, N)
+        for whole in [False, True]:
+            for worN in [N, np.linspace(0, 1, N)]:
+                w, h = freqz(b, worN=worN, whole=whole)
+                assert_equal(w.size, N)
+                for k in range(N):
+                    bk = b[:, k]
+                    ww, hh = freqz(bk, worN=w[k], whole=whole)
+                    assert_allclose(ww, w[k])
+                    assert_allclose(hh, h[k])
+
+    def test_broadcasting4(self):
+        # Test broadcasting with worN a 2-D array.
+        np.random.seed(123)
+        b = np.random.rand(4, 2, 1, 1)
+        a = np.random.rand(5, 2, 1, 1)
+        for whole in [False, True]:
+            for worN in [np.random.rand(6, 7), np.empty((6, 0))]:
+                w, h = freqz(b, a, worN=worN, whole=whole)
+                assert_allclose(w, worN, rtol=1e-14)
+                assert_equal(h.shape, (2,) + worN.shape)
+                for k in range(2):
+                    ww, hh = freqz(b[:, k, 0, 0], a[:, k, 0, 0],
+                                   worN=worN.ravel(),
+                                   whole=whole)
+                    assert_allclose(ww, worN.ravel(), rtol=1e-14)
+                    assert_allclose(hh, h[k, :, :].ravel())
+
+    def test_backward_compat(self):
+        # For backward compatibility, test if None act as a wrapper for default
+        w1, h1 = freqz([1.0], 1)
+        w2, h2 = freqz([1.0], 1, None)
+        assert_array_almost_equal(w1, w2)
+        assert_array_almost_equal(h1, h2)
+
+    def test_fs_param(self):
+        fs = 900
+        b = [0.039479155677484369, 0.11843746703245311, 0.11843746703245311,
+             0.039479155677484369]
+        a = [1.0, -1.3199152021838287, 0.80341991081938424,
+             -0.16767146321568049]
+
+        # N = None, whole=False
+        w1, h1 = freqz(b, a, fs=fs)
+        w2, h2 = freqz(b, a)
+        assert_allclose(h1, h2)
+        assert_allclose(w1, np.linspace(0, fs/2, 512, endpoint=False))
+
+        # N = None, whole=True
+        w1, h1 = freqz(b, a, whole=True, fs=fs)
+        w2, h2 = freqz(b, a, whole=True)
+        assert_allclose(h1, h2)
+        assert_allclose(w1, np.linspace(0, fs, 512, endpoint=False))
+
+        # N = 5, whole=False
+        w1, h1 = freqz(b, a, 5, fs=fs)
+        w2, h2 = freqz(b, a, 5)
+        assert_allclose(h1, h2)
+        assert_allclose(w1, np.linspace(0, fs/2, 5, endpoint=False))
+
+        # N = 5, whole=True
+        w1, h1 = freqz(b, a, 5, whole=True, fs=fs)
+        w2, h2 = freqz(b, a, 5, whole=True)
+        assert_allclose(h1, h2)
+        assert_allclose(w1, np.linspace(0, fs, 5, endpoint=False))
+
+        # w is an array_like
+        for w in ([123], (123,), np.array([123]), (50, 123, 230),
+                  np.array([50, 123, 230])):
+            w1, h1 = freqz(b, a, w, fs=fs)
+            w2, h2 = freqz(b, a, 2*pi*np.array(w)/fs)
+            assert_allclose(h1, h2)
+            assert_allclose(w, w1)
+
+    def test_w_or_N_types(self):
+        # Measure at 7 (polyval) or 8 (fft) equally-spaced points
+        for N in (7, np.int8(7), np.int16(7), np.int32(7), np.int64(7),
+                  np.array(7),
+                  8, np.int8(8), np.int16(8), np.int32(8), np.int64(8),
+                  np.array(8)):
+
+            w, h = freqz([1.0], worN=N)
+            assert_array_almost_equal(w, np.pi * np.arange(N) / N)
+            assert_array_almost_equal(h, np.ones(N))
+
+            w, h = freqz([1.0], worN=N, fs=100)
+            assert_array_almost_equal(w, np.linspace(0, 50, N, endpoint=False))
+            assert_array_almost_equal(h, np.ones(N))
+
+        # Measure at frequency 8 Hz
+        for w in (8.0, 8.0+0j):
+            # Only makes sense when fs is specified
+            w_out, h = freqz([1.0], worN=w, fs=100)
+            assert_array_almost_equal(w_out, [8])
+            assert_array_almost_equal(h, [1])
+
+    def test_nyquist(self):
+        w, h = freqz([1.0], worN=8, include_nyquist=True)
+        assert_array_almost_equal(w, np.pi * np.arange(8) / 7.)
+        assert_array_almost_equal(h, np.ones(8))
+        w, h = freqz([1.0], worN=9, include_nyquist=True)
+        assert_array_almost_equal(w, np.pi * np.arange(9) / 8.)
+        assert_array_almost_equal(h, np.ones(9))
+
+        for a in [1, np.ones(2)]:
+            w, h = freqz(np.ones(2), a, worN=0, include_nyquist=True)
+            assert_equal(w.shape, (0,))
+            assert_equal(h.shape, (0,))
+            assert_equal(h.dtype, np.dtype('complex128'))
+
+        w1, h1 = freqz([1.0], worN=8, whole = True, include_nyquist=True)
+        w2, h2 = freqz([1.0], worN=8, whole = True, include_nyquist=False)
+        assert_array_almost_equal(w1, w2)
+        assert_array_almost_equal(h1, h2)
+
+    # https://github.com/scipy/scipy/issues/17289
+    # https://github.com/scipy/scipy/issues/15273
+    @pytest.mark.parametrize('whole,nyquist,worN',
+                             [(False, False, 32),
+                              (False, True, 32),
+                              (True, False, 32),
+                              (True, True, 32),
+                              (False, False, 257),
+                              (False, True, 257),
+                              (True, False, 257),
+                              (True, True, 257)])
+    def test_17289(self, whole, nyquist, worN):
+        d = [0, 1]
+        w, Drfft = freqz(d, worN=32, whole=whole, include_nyquist=nyquist)
+        _, Dpoly = freqz(d, worN=w)
+        assert_allclose(Drfft, Dpoly)
+
+    def test_fs_validation(self):
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            freqz([1.0], fs=np.array([10, 20]))
+
+        with pytest.raises(ValueError, match="Sampling.*be none."):
+            freqz([1.0], fs=None)
+
+
+class TestSOSFreqz:
+
+    def test_sosfreqz_basic(self):
+        # Compare the results of freqz and sosfreqz for a low order
+        # Butterworth filter.
+
+        N = 500
+
+        b, a = butter(4, 0.2)
+        sos = butter(4, 0.2, output='sos')
+        w, h = freqz(b, a, worN=N)
+        w2, h2 = sosfreqz(sos, worN=N)
+        assert_equal(w2, w)
+        assert_allclose(h2, h, rtol=1e-10, atol=1e-14)
+
+        b, a = ellip(3, 1, 30, (0.2, 0.3), btype='bandpass')
+        sos = ellip(3, 1, 30, (0.2, 0.3), btype='bandpass', output='sos')
+        w, h = freqz(b, a, worN=N)
+        w2, h2 = sosfreqz(sos, worN=N)
+        assert_equal(w2, w)
+        assert_allclose(h2, h, rtol=1e-10, atol=1e-14)
+        # must have at least one section
+        assert_raises(ValueError, sosfreqz, sos[:0])
+
+    def test_sosfrez_design(self):
+        # Compare sosfreqz output against expected values for different
+        # filter types
+
+        # from cheb2ord
+        N, Wn = cheb2ord([0.1, 0.6], [0.2, 0.5], 3, 60)
+        sos = cheby2(N, 60, Wn, 'stop', output='sos')
+        w, h = sosfreqz(sos)
+        h = np.abs(h)
+        w /= np.pi
+        assert_allclose(20 * np.log10(h[w <= 0.1]), 0, atol=3.01)
+        assert_allclose(20 * np.log10(h[w >= 0.6]), 0., atol=3.01)
+        assert_allclose(h[(w >= 0.2) & (w <= 0.5)], 0., atol=1e-3)  # <= -60 dB
+
+        N, Wn = cheb2ord([0.1, 0.6], [0.2, 0.5], 3, 150)
+        sos = cheby2(N, 150, Wn, 'stop', output='sos')
+        w, h = sosfreqz(sos)
+        dB = 20*np.log10(np.abs(h))
+        w /= np.pi
+        assert_allclose(dB[w <= 0.1], 0, atol=3.01)
+        assert_allclose(dB[w >= 0.6], 0., atol=3.01)
+        assert_array_less(dB[(w >= 0.2) & (w <= 0.5)], -149.9)
+
+        # from cheb1ord
+        N, Wn = cheb1ord(0.2, 0.3, 3, 40)
+        sos = cheby1(N, 3, Wn, 'low', output='sos')
+        w, h = sosfreqz(sos)
+        h = np.abs(h)
+        w /= np.pi
+        assert_allclose(20 * np.log10(h[w <= 0.2]), 0, atol=3.01)
+        assert_allclose(h[w >= 0.3], 0., atol=1e-2)  # <= -40 dB
+
+        N, Wn = cheb1ord(0.2, 0.3, 1, 150)
+        sos = cheby1(N, 1, Wn, 'low', output='sos')
+        w, h = sosfreqz(sos)
+        dB = 20*np.log10(np.abs(h))
+        w /= np.pi
+        assert_allclose(dB[w <= 0.2], 0, atol=1.01)
+        assert_array_less(dB[w >= 0.3], -149.9)
+
+        # adapted from ellipord
+        N, Wn = ellipord(0.3, 0.2, 3, 60)
+        sos = ellip(N, 0.3, 60, Wn, 'high', output='sos')
+        w, h = sosfreqz(sos)
+        h = np.abs(h)
+        w /= np.pi
+        assert_allclose(20 * np.log10(h[w >= 0.3]), 0, atol=3.01)
+        assert_allclose(h[w <= 0.1], 0., atol=1.5e-3)  # <= -60 dB (approx)
+
+        # adapted from buttord
+        N, Wn = buttord([0.2, 0.5], [0.14, 0.6], 3, 40)
+        sos = butter(N, Wn, 'band', output='sos')
+        w, h = sosfreqz(sos)
+        h = np.abs(h)
+        w /= np.pi
+        assert_allclose(h[w <= 0.14], 0., atol=1e-2)  # <= -40 dB
+        assert_allclose(h[w >= 0.6], 0., atol=1e-2)  # <= -40 dB
+        assert_allclose(20 * np.log10(h[(w >= 0.2) & (w <= 0.5)]),
+                        0, atol=3.01)
+
+        N, Wn = buttord([0.2, 0.5], [0.14, 0.6], 3, 100)
+        sos = butter(N, Wn, 'band', output='sos')
+        w, h = sosfreqz(sos)
+        dB = 20*np.log10(np.maximum(np.abs(h), 1e-10))
+        w /= np.pi
+        assert_array_less(dB[(w > 0) & (w <= 0.14)], -99.9)
+        assert_array_less(dB[w >= 0.6], -99.9)
+        assert_allclose(dB[(w >= 0.2) & (w <= 0.5)], 0, atol=3.01)
+
+    def test_sosfreqz_design_ellip(self):
+        N, Wn = ellipord(0.3, 0.1, 3, 60)
+        sos = ellip(N, 0.3, 60, Wn, 'high', output='sos')
+        w, h = sosfreqz(sos)
+        h = np.abs(h)
+        w /= np.pi
+        assert_allclose(20 * np.log10(h[w >= 0.3]), 0, atol=3.01)
+        assert_allclose(h[w <= 0.1], 0., atol=1.5e-3)  # <= -60 dB (approx)
+
+        N, Wn = ellipord(0.3, 0.2, .5, 150)
+        sos = ellip(N, .5, 150, Wn, 'high', output='sos')
+        w, h = sosfreqz(sos)
+        dB = 20*np.log10(np.maximum(np.abs(h), 1e-10))
+        w /= np.pi
+        assert_allclose(dB[w >= 0.3], 0, atol=.55)
+        # Allow some numerical slop in the upper bound -150, so this is
+        # a check that dB[w <= 0.2] is less than or almost equal to -150.
+        assert dB[w <= 0.2].max() < -150*(1 - 1e-12)
+
+    @mpmath_check("0.10")
+    def test_sos_freqz_against_mp(self):
+        # Compare the result of sosfreqz applied to a high order Butterworth
+        # filter against the result computed using mpmath.  (signal.freqz fails
+        # miserably with such high order filters.)
+        from . import mpsig
+        N = 500
+        order = 25
+        Wn = 0.15
+        with mpmath.workdps(80):
+            z_mp, p_mp, k_mp = mpsig.butter_lp(order, Wn)
+            w_mp, h_mp = mpsig.zpkfreqz(z_mp, p_mp, k_mp, N)
+        w_mp = np.array([float(x) for x in w_mp])
+        h_mp = np.array([complex(x) for x in h_mp])
+
+        sos = butter(order, Wn, output='sos')
+        w, h = sosfreqz(sos, worN=N)
+        assert_allclose(w, w_mp, rtol=1e-12, atol=1e-14)
+        assert_allclose(h, h_mp, rtol=1e-12, atol=1e-14)
+
+    def test_fs_param(self):
+        fs = 900
+        sos = [[0.03934683014103762, 0.07869366028207524, 0.03934683014103762,
+                1.0, -0.37256600288916636, 0.0],
+               [1.0, 1.0, 0.0, 1.0, -0.9495739996946778, 0.45125966317124144]]
+
+        # N = None, whole=False
+        w1, h1 = sosfreqz(sos, fs=fs)
+        w2, h2 = sosfreqz(sos)
+        assert_allclose(h1, h2)
+        assert_allclose(w1, np.linspace(0, fs/2, 512, endpoint=False))
+
+        # N = None, whole=True
+        w1, h1 = sosfreqz(sos, whole=True, fs=fs)
+        w2, h2 = sosfreqz(sos, whole=True)
+        assert_allclose(h1, h2, atol=1e-27)
+        assert_allclose(w1, np.linspace(0, fs, 512, endpoint=False))
+
+        # N = 5, whole=False
+        w1, h1 = sosfreqz(sos, 5, fs=fs)
+        w2, h2 = sosfreqz(sos, 5)
+        assert_allclose(h1, h2)
+        assert_allclose(w1, np.linspace(0, fs/2, 5, endpoint=False))
+
+        # N = 5, whole=True
+        w1, h1 = sosfreqz(sos, 5, whole=True, fs=fs)
+        w2, h2 = sosfreqz(sos, 5, whole=True)
+        assert_allclose(h1, h2)
+        assert_allclose(w1, np.linspace(0, fs, 5, endpoint=False))
+
+        # w is an array_like
+        for w in ([123], (123,), np.array([123]), (50, 123, 230),
+                  np.array([50, 123, 230])):
+            w1, h1 = sosfreqz(sos, w, fs=fs)
+            w2, h2 = sosfreqz(sos, 2*pi*np.array(w)/fs)
+            assert_allclose(h1, h2)
+            assert_allclose(w, w1)
+
+    def test_w_or_N_types(self):
+        # Measure at 7 (polyval) or 8 (fft) equally-spaced points
+        for N in (7, np.int8(7), np.int16(7), np.int32(7), np.int64(7),
+                  np.array(7),
+                  8, np.int8(8), np.int16(8), np.int32(8), np.int64(8),
+                  np.array(8)):
+
+            w, h = sosfreqz([1, 0, 0, 1, 0, 0], worN=N)
+            assert_array_almost_equal(w, np.pi * np.arange(N) / N)
+            assert_array_almost_equal(h, np.ones(N))
+
+            w, h = sosfreqz([1, 0, 0, 1, 0, 0], worN=N, fs=100)
+            assert_array_almost_equal(w, np.linspace(0, 50, N, endpoint=False))
+            assert_array_almost_equal(h, np.ones(N))
+
+        # Measure at frequency 8 Hz
+        for w in (8.0, 8.0+0j):
+            # Only makes sense when fs is specified
+            w_out, h = sosfreqz([1, 0, 0, 1, 0, 0], worN=w, fs=100)
+            assert_array_almost_equal(w_out, [8])
+            assert_array_almost_equal(h, [1])
+
+    def test_fs_validation(self):
+        sos = butter(4, 0.2, output='sos')
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            sosfreqz(sos, fs=np.array([10, 20]))
+
+
+class TestFreqz_zpk:
+
+    def test_ticket1441(self):
+        """Regression test for ticket 1441."""
+        # Because freqz previously used arange instead of linspace,
+        # when N was large, it would return one more point than
+        # requested.
+        N = 100000
+        w, h = freqz_zpk([0.5], [0.5], 1.0, worN=N)
+        assert_equal(w.shape, (N,))
+
+    def test_basic(self):
+        w, h = freqz_zpk([0.5], [0.5], 1.0, worN=8)
+        assert_array_almost_equal(w, np.pi * np.arange(8.0) / 8)
+        assert_array_almost_equal(h, np.ones(8))
+
+    def test_basic_whole(self):
+        w, h = freqz_zpk([0.5], [0.5], 1.0, worN=8, whole=True)
+        assert_array_almost_equal(w, 2 * np.pi * np.arange(8.0) / 8)
+        assert_array_almost_equal(h, np.ones(8))
+
+    def test_vs_freqz(self):
+        b, a = cheby1(4, 5, 0.5, analog=False, output='ba')
+        z, p, k = cheby1(4, 5, 0.5, analog=False, output='zpk')
+
+        w1, h1 = freqz(b, a)
+        w2, h2 = freqz_zpk(z, p, k)
+        assert_allclose(w1, w2)
+        assert_allclose(h1, h2, rtol=1e-6)
+
+    def test_backward_compat(self):
+        # For backward compatibility, test if None act as a wrapper for default
+        w1, h1 = freqz_zpk([0.5], [0.5], 1.0)
+        w2, h2 = freqz_zpk([0.5], [0.5], 1.0, None)
+        assert_array_almost_equal(w1, w2)
+        assert_array_almost_equal(h1, h2)
+
+    def test_fs_param(self):
+        fs = 900
+        z = [-1, -1, -1]
+        p = [0.4747869998473389+0.4752230717749344j, 0.37256600288916636,
+             0.4747869998473389-0.4752230717749344j]
+        k = 0.03934683014103762
+
+        # N = None, whole=False
+        w1, h1 = freqz_zpk(z, p, k, whole=False, fs=fs)
+        w2, h2 = freqz_zpk(z, p, k, whole=False)
+        assert_allclose(h1, h2)
+        assert_allclose(w1, np.linspace(0, fs/2, 512, endpoint=False))
+
+        # N = None, whole=True
+        w1, h1 = freqz_zpk(z, p, k, whole=True, fs=fs)
+        w2, h2 = freqz_zpk(z, p, k, whole=True)
+        assert_allclose(h1, h2)
+        assert_allclose(w1, np.linspace(0, fs, 512, endpoint=False))
+
+        # N = 5, whole=False
+        w1, h1 = freqz_zpk(z, p, k, 5, fs=fs)
+        w2, h2 = freqz_zpk(z, p, k, 5)
+        assert_allclose(h1, h2)
+        assert_allclose(w1, np.linspace(0, fs/2, 5, endpoint=False))
+
+        # N = 5, whole=True
+        w1, h1 = freqz_zpk(z, p, k, 5, whole=True, fs=fs)
+        w2, h2 = freqz_zpk(z, p, k, 5, whole=True)
+        assert_allclose(h1, h2)
+        assert_allclose(w1, np.linspace(0, fs, 5, endpoint=False))
+
+        # w is an array_like
+        for w in ([123], (123,), np.array([123]), (50, 123, 230),
+                  np.array([50, 123, 230])):
+            w1, h1 = freqz_zpk(z, p, k, w, fs=fs)
+            w2, h2 = freqz_zpk(z, p, k, 2*pi*np.array(w)/fs)
+            assert_allclose(h1, h2)
+            assert_allclose(w, w1)
+
+    def test_w_or_N_types(self):
+        # Measure at 8 equally-spaced points
+        for N in (8, np.int8(8), np.int16(8), np.int32(8), np.int64(8),
+                  np.array(8)):
+
+            w, h = freqz_zpk([], [], 1, worN=N)
+            assert_array_almost_equal(w, np.pi * np.arange(8) / 8.)
+            assert_array_almost_equal(h, np.ones(8))
+
+            w, h = freqz_zpk([], [], 1, worN=N, fs=100)
+            assert_array_almost_equal(w, np.linspace(0, 50, 8, endpoint=False))
+            assert_array_almost_equal(h, np.ones(8))
+
+        # Measure at frequency 8 Hz
+        for w in (8.0, 8.0+0j):
+            # Only makes sense when fs is specified
+            w_out, h = freqz_zpk([], [], 1, worN=w, fs=100)
+            assert_array_almost_equal(w_out, [8])
+            assert_array_almost_equal(h, [1])
+
+    def test_fs_validation(self):
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            freqz_zpk([1.0], [1.0], [1.0], fs=np.array([10, 20]))
+
+        with pytest.raises(ValueError, match="Sampling.*be none."):
+            freqz_zpk([1.0], [1.0], [1.0], fs=None)
+
+
+class TestNormalize:
+
+    def test_allclose(self):
+        """Test for false positive on allclose in normalize() in
+        filter_design.py"""
+        # Test to make sure the allclose call within signal.normalize does not
+        # choose false positives. Then check against a known output from MATLAB
+        # to make sure the fix doesn't break anything.
+
+        # These are the coefficients returned from
+        #   `[b,a] = cheby1(8, 0.5, 0.048)'
+        # in MATLAB. There are at least 15 significant figures in each
+        # coefficient, so it makes sense to test for errors on the order of
+        # 1e-13 (this can always be relaxed if different platforms have
+        # different rounding errors)
+        b_matlab = np.array([2.150733144728282e-11, 1.720586515782626e-10,
+                             6.022052805239190e-10, 1.204410561047838e-09,
+                             1.505513201309798e-09, 1.204410561047838e-09,
+                             6.022052805239190e-10, 1.720586515782626e-10,
+                             2.150733144728282e-11])
+        a_matlab = np.array([1.000000000000000e+00, -7.782402035027959e+00,
+                             2.654354569747454e+01, -5.182182531666387e+01,
+                             6.334127355102684e+01, -4.963358186631157e+01,
+                             2.434862182949389e+01, -6.836925348604676e+00,
+                             8.412934944449140e-01])
+
+        # This is the input to signal.normalize after passing through the
+        # equivalent steps in signal.iirfilter as was done for MATLAB
+        b_norm_in = np.array([1.5543135865293012e-06, 1.2434508692234413e-05,
+                              4.3520780422820447e-05, 8.7041560845640893e-05,
+                              1.0880195105705122e-04, 8.7041560845640975e-05,
+                              4.3520780422820447e-05, 1.2434508692234413e-05,
+                              1.5543135865293012e-06])
+        a_norm_in = np.array([7.2269025909127173e+04, -5.6242661430467968e+05,
+                              1.9182761917308895e+06, -3.7451128364682454e+06,
+                              4.5776121393762771e+06, -3.5869706138592605e+06,
+                              1.7596511818472347e+06, -4.9409793515707983e+05,
+                              6.0799461347219651e+04])
+
+        b_output, a_output = normalize(b_norm_in, a_norm_in)
+
+        # The test on b works for decimal=14 but the one for a does not. For
+        # the sake of consistency, both of these are decimal=13. If something
+        # breaks on another platform, it is probably fine to relax this lower.
+        assert_array_almost_equal(b_matlab, b_output, decimal=13)
+        assert_array_almost_equal(a_matlab, a_output, decimal=13)
+
+    def test_errors(self):
+        """Test the error cases."""
+        # all zero denominator
+        assert_raises(ValueError, normalize, [1, 2], 0)
+
+        # denominator not 1 dimensional
+        assert_raises(ValueError, normalize, [1, 2], [[1]])
+
+        # numerator too many dimensions
+        assert_raises(ValueError, normalize, [[[1, 2]]], 1)
+
+
+class TestLp2lp:
+
+    def test_basic(self):
+        b = [1]
+        a = [1, np.sqrt(2), 1]
+        b_lp, a_lp = lp2lp(b, a, 0.38574256627112119)
+        assert_array_almost_equal(b_lp, [0.1488], decimal=4)
+        assert_array_almost_equal(a_lp, [1, 0.5455, 0.1488], decimal=4)
+
+
+class TestLp2hp:
+
+    def test_basic(self):
+        b = [0.25059432325190018]
+        a = [1, 0.59724041654134863, 0.92834805757524175, 0.25059432325190018]
+        b_hp, a_hp = lp2hp(b, a, 2*np.pi*5000)
+        assert_allclose(b_hp, [1, 0, 0, 0])
+        assert_allclose(a_hp, [1, 1.1638e5, 2.3522e9, 1.2373e14], rtol=1e-4)
+
+
+class TestLp2bp:
+
+    def test_basic(self):
+        b = [1]
+        a = [1, 2, 2, 1]
+        b_bp, a_bp = lp2bp(b, a, 2*np.pi*4000, 2*np.pi*2000)
+        assert_allclose(b_bp, [1.9844e12, 0, 0, 0], rtol=1e-6)
+        assert_allclose(a_bp, [1, 2.5133e4, 2.2108e9, 3.3735e13,
+                               1.3965e18, 1.0028e22, 2.5202e26], rtol=1e-4)
+
+
+class TestLp2bs:
+
+    def test_basic(self):
+        b = [1]
+        a = [1, 1]
+        b_bs, a_bs = lp2bs(b, a, 0.41722257286366754, 0.18460575326152251)
+        assert_array_almost_equal(b_bs, [1, 0, 0.17407], decimal=5)
+        assert_array_almost_equal(a_bs, [1, 0.18461, 0.17407], decimal=5)
+
+
+class TestBilinear:
+
+    def test_basic(self):
+        b = [0.14879732743343033]
+        a = [1, 0.54552236880522209, 0.14879732743343033]
+        b_z, a_z = bilinear(b, a, 0.5)
+        assert_array_almost_equal(b_z, [0.087821, 0.17564, 0.087821],
+                                  decimal=5)
+        assert_array_almost_equal(a_z, [1, -1.0048, 0.35606], decimal=4)
+
+        b = [1, 0, 0.17407467530697837]
+        a = [1, 0.18460575326152251, 0.17407467530697837]
+        b_z, a_z = bilinear(b, a, 0.5)
+        assert_array_almost_equal(b_z, [0.86413, -1.2158, 0.86413],
+                                  decimal=4)
+        assert_array_almost_equal(a_z, [1, -1.2158, 0.72826],
+                                  decimal=4)
+
+    def test_fs_validation(self):
+        b = [0.14879732743343033]
+        a = [1, 0.54552236880522209, 0.14879732743343033]
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            bilinear(b, a, fs=np.array([10, 20]))
+
+        with pytest.raises(ValueError, match="Sampling.*be none"):
+            bilinear(b, a, fs=None)
+
+
+class TestLp2lp_zpk:
+
+    def test_basic(self):
+        z = []
+        p = [(-1+1j)/np.sqrt(2), (-1-1j)/np.sqrt(2)]
+        k = 1
+        z_lp, p_lp, k_lp = lp2lp_zpk(z, p, k, 5)
+        assert_array_equal(z_lp, [])
+        assert_allclose(sort(p_lp), sort(p)*5)
+        assert_allclose(k_lp, 25)
+
+        # Pseudo-Chebyshev with both poles and zeros
+        z = [-2j, +2j]
+        p = [-0.75, -0.5-0.5j, -0.5+0.5j]
+        k = 3
+        z_lp, p_lp, k_lp = lp2lp_zpk(z, p, k, 20)
+        assert_allclose(sort(z_lp), sort([-40j, +40j]))
+        assert_allclose(sort(p_lp), sort([-15, -10-10j, -10+10j]))
+        assert_allclose(k_lp, 60)
+
+    def test_fs_validation(self):
+        z = [-2j, +2j]
+        p = [-0.75, -0.5 - 0.5j, -0.5 + 0.5j]
+        k = 3
+
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            bilinear_zpk(z, p, k, fs=np.array([10, 20]))
+
+        with pytest.raises(ValueError, match="Sampling.*be none"):
+            bilinear_zpk(z, p, k, fs=None)
+
+
+class TestLp2hp_zpk:
+
+    def test_basic(self):
+        z = []
+        p = [(-1+1j)/np.sqrt(2), (-1-1j)/np.sqrt(2)]
+        k = 1
+
+        z_hp, p_hp, k_hp = lp2hp_zpk(z, p, k, 5)
+        assert_array_equal(z_hp, [0, 0])
+        assert_allclose(sort(p_hp), sort(p)*5)
+        assert_allclose(k_hp, 1)
+
+        z = [-2j, +2j]
+        p = [-0.75, -0.5-0.5j, -0.5+0.5j]
+        k = 3
+        z_hp, p_hp, k_hp = lp2hp_zpk(z, p, k, 6)
+        assert_allclose(sort(z_hp), sort([-3j, 0, +3j]))
+        assert_allclose(sort(p_hp), sort([-8, -6-6j, -6+6j]))
+        assert_allclose(k_hp, 32)
+
+
+class TestLp2bp_zpk:
+
+    def test_basic(self):
+        z = [-2j, +2j]
+        p = [-0.75, -0.5-0.5j, -0.5+0.5j]
+        k = 3
+        z_bp, p_bp, k_bp = lp2bp_zpk(z, p, k, 15, 8)
+        assert_allclose(sort(z_bp), sort([-25j, -9j, 0, +9j, +25j]))
+        assert_allclose(sort(p_bp), sort([-3 + 6j*sqrt(6),
+                                          -3 - 6j*sqrt(6),
+                                          +2j+sqrt(-8j-225)-2,
+                                          -2j+sqrt(+8j-225)-2,
+                                          +2j-sqrt(-8j-225)-2,
+                                          -2j-sqrt(+8j-225)-2, ]))
+        assert_allclose(k_bp, 24)
+
+
+class TestLp2bs_zpk:
+
+    def test_basic(self):
+        z = [-2j, +2j]
+        p = [-0.75, -0.5-0.5j, -0.5+0.5j]
+        k = 3
+
+        z_bs, p_bs, k_bs = lp2bs_zpk(z, p, k, 35, 12)
+
+        assert_allclose(sort(z_bs), sort([+35j, -35j,
+                                          +3j+sqrt(1234)*1j,
+                                          -3j+sqrt(1234)*1j,
+                                          +3j-sqrt(1234)*1j,
+                                          -3j-sqrt(1234)*1j]))
+        assert_allclose(sort(p_bs), sort([+3j*sqrt(129) - 8,
+                                          -3j*sqrt(129) - 8,
+                                          (-6 + 6j) - sqrt(-1225 - 72j),
+                                          (-6 - 6j) - sqrt(-1225 + 72j),
+                                          (-6 + 6j) + sqrt(-1225 - 72j),
+                                          (-6 - 6j) + sqrt(-1225 + 72j), ]))
+        assert_allclose(k_bs, 32)
+
+
+class TestBilinear_zpk:
+
+    def test_basic(self):
+        z = [-2j, +2j]
+        p = [-0.75, -0.5-0.5j, -0.5+0.5j]
+        k = 3
+
+        z_d, p_d, k_d = bilinear_zpk(z, p, k, 10)
+
+        assert_allclose(sort(z_d), sort([(20-2j)/(20+2j), (20+2j)/(20-2j),
+                                         -1]))
+        assert_allclose(sort(p_d), sort([77/83,
+                                         (1j/2 + 39/2) / (41/2 - 1j/2),
+                                         (39/2 - 1j/2) / (1j/2 + 41/2), ]))
+        assert_allclose(k_d, 9696/69803)
+
+
+class TestPrototypeType:
+
+    def test_output_type(self):
+        # Prototypes should consistently output arrays, not lists
+        # https://github.com/scipy/scipy/pull/441
+        for func in (buttap,
+                     besselap,
+                     lambda N: cheb1ap(N, 1),
+                     lambda N: cheb2ap(N, 20),
+                     lambda N: ellipap(N, 1, 20)):
+            for N in range(7):
+                z, p, k = func(N)
+                assert_(isinstance(z, np.ndarray))
+                assert_(isinstance(p, np.ndarray))
+
+
+def dB(x):
+    # Return magnitude in decibels, avoiding divide-by-zero warnings
+    # (and deal with some "not less-ordered" errors when -inf shows up)
+    return 20 * np.log10(np.maximum(np.abs(x), np.finfo(np.float64).tiny))
+
+
+class TestButtord:
+
+    def test_lowpass(self):
+        wp = 0.2
+        ws = 0.3
+        rp = 3
+        rs = 60
+        N, Wn = buttord(wp, ws, rp, rs, False)
+        b, a = butter(N, Wn, 'lowpass', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp, dB(h[w <= wp]))
+        assert_array_less(dB(h[ws <= w]), -rs)
+
+        assert_equal(N, 16)
+        assert_allclose(Wn, 2.0002776782743284e-01, rtol=1e-15)
+
+    def test_highpass(self):
+        wp = 0.3
+        ws = 0.2
+        rp = 3
+        rs = 70
+        N, Wn = buttord(wp, ws, rp, rs, False)
+        b, a = butter(N, Wn, 'highpass', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp, dB(h[wp <= w]))
+        assert_array_less(dB(h[w <= ws]), -rs)
+
+        assert_equal(N, 18)
+        assert_allclose(Wn, 2.9996603079132672e-01, rtol=1e-15)
+
+    def test_bandpass(self):
+        wp = [0.2, 0.5]
+        ws = [0.1, 0.6]
+        rp = 3
+        rs = 80
+        N, Wn = buttord(wp, ws, rp, rs, False)
+        b, a = butter(N, Wn, 'bandpass', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1,
+                          dB(h[np.logical_and(wp[0] <= w, w <= wp[1])]))
+        assert_array_less(dB(h[np.logical_or(w <= ws[0], ws[1] <= w)]),
+                          -rs + 0.1)
+
+        assert_equal(N, 18)
+        assert_allclose(Wn, [1.9998742411409134e-01, 5.0002139595676276e-01],
+                        rtol=1e-15)
+
+    def test_bandstop(self):
+        wp = [0.1, 0.6]
+        ws = [0.2, 0.5]
+        rp = 3
+        rs = 90
+        N, Wn = buttord(wp, ws, rp, rs, False)
+        b, a = butter(N, Wn, 'bandstop', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp,
+                          dB(h[np.logical_or(w <= wp[0], wp[1] <= w)]))
+        assert_array_less(dB(h[np.logical_and(ws[0] <= w, w <= ws[1])]),
+                          -rs)
+
+        assert_equal(N, 20)
+        assert_allclose(Wn, [1.4759432329294042e-01, 5.9997365985276407e-01],
+                        rtol=1e-6)
+
+    def test_analog(self):
+        wp = 200
+        ws = 600
+        rp = 3
+        rs = 60
+        N, Wn = buttord(wp, ws, rp, rs, True)
+        b, a = butter(N, Wn, 'lowpass', True)
+        w, h = freqs(b, a)
+        assert_array_less(-rp, dB(h[w <= wp]))
+        assert_array_less(dB(h[ws <= w]), -rs)
+
+        assert_equal(N, 7)
+        assert_allclose(Wn, 2.0006785355671877e+02, rtol=1e-15)
+
+        n, Wn = buttord(1, 550/450, 1, 26, analog=True)
+        assert_equal(n, 19)
+        assert_allclose(Wn, 1.0361980524629517, rtol=1e-15)
+
+        assert_equal(buttord(1, 1.2, 1, 80, analog=True)[0], 55)
+
+    def test_fs_param(self):
+        wp = [4410, 11025]
+        ws = [2205, 13230]
+        rp = 3
+        rs = 80
+        fs = 44100
+        N, Wn = buttord(wp, ws, rp, rs, False, fs=fs)
+        b, a = butter(N, Wn, 'bandpass', False, fs=fs)
+        w, h = freqz(b, a, fs=fs)
+        assert_array_less(-rp - 0.1,
+                          dB(h[np.logical_and(wp[0] <= w, w <= wp[1])]))
+        assert_array_less(dB(h[np.logical_or(w <= ws[0], ws[1] <= w)]),
+                          -rs + 0.1)
+
+        assert_equal(N, 18)
+        assert_allclose(Wn, [4409.722701715714, 11025.47178084662],
+                        rtol=1e-15)
+
+    def test_invalid_input(self):
+        with pytest.raises(ValueError) as exc_info:
+            buttord([20, 50], [14, 60], 3, 2)
+        assert "gpass should be smaller than gstop" in str(exc_info.value)
+
+        with pytest.raises(ValueError) as exc_info:
+            buttord([20, 50], [14, 60], -1, 2)
+        assert "gpass should be larger than 0.0" in str(exc_info.value)
+
+        with pytest.raises(ValueError) as exc_info:
+            buttord([20, 50], [14, 60], 1, -2)
+        assert "gstop should be larger than 0.0" in str(exc_info.value)
+
+    def test_runtime_warnings(self):
+        msg = "Order is zero.*|divide by zero encountered"
+        with pytest.warns(RuntimeWarning, match=msg):
+            buttord(0.0, 1.0, 3, 60)
+
+    def test_ellip_butter(self):
+        # The purpose of the test is to compare to some known output from past
+        # scipy versions. The values to compare to are generated with scipy
+        # 1.9.1 (there is nothing special about this particular version though)
+        n, wn = buttord([0.1, 0.6], [0.2, 0.5], 3, 60)
+        assert n == 14
+
+    def test_fs_validation(self):
+        wp = 0.2
+        ws = 0.3
+        rp = 3
+        rs = 60
+
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            buttord(wp, ws, rp, rs, False, fs=np.array([10, 20]))
+
+
+class TestCheb1ord:
+
+    def test_lowpass(self):
+        wp = 0.2
+        ws = 0.3
+        rp = 3
+        rs = 60
+        N, Wn = cheb1ord(wp, ws, rp, rs, False)
+        b, a = cheby1(N, rp, Wn, 'low', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1, dB(h[w <= wp]))
+        assert_array_less(dB(h[ws <= w]), -rs + 0.1)
+
+        assert_equal(N, 8)
+        assert_allclose(Wn, 0.2, rtol=1e-15)
+
+    def test_highpass(self):
+        wp = 0.3
+        ws = 0.2
+        rp = 3
+        rs = 70
+        N, Wn = cheb1ord(wp, ws, rp, rs, False)
+        b, a = cheby1(N, rp, Wn, 'high', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1, dB(h[wp <= w]))
+        assert_array_less(dB(h[w <= ws]), -rs + 0.1)
+
+        assert_equal(N, 9)
+        assert_allclose(Wn, 0.3, rtol=1e-15)
+
+    def test_bandpass(self):
+        wp = [0.2, 0.5]
+        ws = [0.1, 0.6]
+        rp = 3
+        rs = 80
+        N, Wn = cheb1ord(wp, ws, rp, rs, False)
+        b, a = cheby1(N, rp, Wn, 'band', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1,
+                          dB(h[np.logical_and(wp[0] <= w, w <= wp[1])]))
+        assert_array_less(dB(h[np.logical_or(w <= ws[0], ws[1] <= w)]),
+                          -rs + 0.1)
+
+        assert_equal(N, 9)
+        assert_allclose(Wn, [0.2, 0.5], rtol=1e-15)
+
+    def test_bandstop(self):
+        wp = [0.1, 0.6]
+        ws = [0.2, 0.5]
+        rp = 3
+        rs = 90
+        N, Wn = cheb1ord(wp, ws, rp, rs, False)
+        b, a = cheby1(N, rp, Wn, 'stop', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1,
+                          dB(h[np.logical_or(w <= wp[0], wp[1] <= w)]))
+        assert_array_less(dB(h[np.logical_and(ws[0] <= w, w <= ws[1])]),
+                          -rs + 0.1)
+
+        assert_equal(N, 10)
+        assert_allclose(Wn, [0.14758232569947785, 0.6], rtol=1e-5)
+
+    def test_analog(self):
+        wp = 700
+        ws = 100
+        rp = 3
+        rs = 70
+        N, Wn = cheb1ord(wp, ws, rp, rs, True)
+        b, a = cheby1(N, rp, Wn, 'high', True)
+        w, h = freqs(b, a)
+        assert_array_less(-rp - 0.1, dB(h[wp <= w]))
+        assert_array_less(dB(h[w <= ws]), -rs + 0.1)
+
+        assert_equal(N, 4)
+        assert_allclose(Wn, 700, rtol=1e-15)
+
+        assert_equal(cheb1ord(1, 1.2, 1, 80, analog=True)[0], 17)
+
+    def test_fs_param(self):
+        wp = 4800
+        ws = 7200
+        rp = 3
+        rs = 60
+        fs = 48000
+        N, Wn = cheb1ord(wp, ws, rp, rs, False, fs=fs)
+        b, a = cheby1(N, rp, Wn, 'low', False, fs=fs)
+        w, h = freqz(b, a, fs=fs)
+        assert_array_less(-rp - 0.1, dB(h[w <= wp]))
+        assert_array_less(dB(h[ws <= w]), -rs + 0.1)
+
+        assert_equal(N, 8)
+        assert_allclose(Wn, 4800, rtol=1e-15)
+
+    def test_invalid_input(self):
+        with pytest.raises(ValueError) as exc_info:
+            cheb1ord(0.2, 0.3, 3, 2)
+        assert "gpass should be smaller than gstop" in str(exc_info.value)
+
+        with pytest.raises(ValueError) as exc_info:
+            cheb1ord(0.2, 0.3, -1, 2)
+        assert "gpass should be larger than 0.0" in str(exc_info.value)
+
+        with pytest.raises(ValueError) as exc_info:
+            cheb1ord(0.2, 0.3, 1, -2)
+        assert "gstop should be larger than 0.0" in str(exc_info.value)
+
+    def test_ellip_cheb1(self):
+        # The purpose of the test is to compare to some known output from past
+        # scipy versions. The values to compare to are generated with scipy
+        # 1.9.1 (there is nothing special about this particular version though)
+        n, wn = cheb1ord([0.1, 0.6], [0.2, 0.5], 3, 60)
+        assert n == 7
+
+        n2, w2 = cheb2ord([0.1, 0.6], [0.2, 0.5], 3, 60)
+        assert not (wn == w2).all()
+
+    def test_fs_validation(self):
+        wp = 0.2
+        ws = 0.3
+        rp = 3
+        rs = 60
+
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            cheb1ord(wp, ws, rp, rs, False, fs=np.array([10, 20]))
+
+
+class TestCheb2ord:
+
+    def test_lowpass(self):
+        wp = 0.2
+        ws = 0.3
+        rp = 3
+        rs = 60
+        N, Wn = cheb2ord(wp, ws, rp, rs, False)
+        b, a = cheby2(N, rs, Wn, 'lp', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1, dB(h[w <= wp]))
+        assert_array_less(dB(h[ws <= w]), -rs + 0.1)
+
+        assert_equal(N, 8)
+        assert_allclose(Wn, 0.28647639976553163, rtol=1e-15)
+
+    def test_highpass(self):
+        wp = 0.3
+        ws = 0.2
+        rp = 3
+        rs = 70
+        N, Wn = cheb2ord(wp, ws, rp, rs, False)
+        b, a = cheby2(N, rs, Wn, 'hp', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1, dB(h[wp <= w]))
+        assert_array_less(dB(h[w <= ws]), -rs + 0.1)
+
+        assert_equal(N, 9)
+        assert_allclose(Wn, 0.20697492182903282, rtol=1e-15)
+
+    def test_bandpass(self):
+        wp = [0.2, 0.5]
+        ws = [0.1, 0.6]
+        rp = 3
+        rs = 80
+        N, Wn = cheb2ord(wp, ws, rp, rs, False)
+        b, a = cheby2(N, rs, Wn, 'bp', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1,
+                          dB(h[np.logical_and(wp[0] <= w, w <= wp[1])]))
+        assert_array_less(dB(h[np.logical_or(w <= ws[0], ws[1] <= w)]),
+                          -rs + 0.1)
+
+        assert_equal(N, 9)
+        assert_allclose(Wn, [0.14876937565923479, 0.59748447842351482],
+                        rtol=1e-15)
+
+    def test_bandstop(self):
+        wp = [0.1, 0.6]
+        ws = [0.2, 0.5]
+        rp = 3
+        rs = 90
+        N, Wn = cheb2ord(wp, ws, rp, rs, False)
+        b, a = cheby2(N, rs, Wn, 'bs', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1,
+                          dB(h[np.logical_or(w <= wp[0], wp[1] <= w)]))
+        assert_array_less(dB(h[np.logical_and(ws[0] <= w, w <= ws[1])]),
+                          -rs + 0.1)
+
+        assert_equal(N, 10)
+        assert_allclose(Wn, [0.19926249974781743, 0.50125246585567362],
+                        rtol=1e-6)
+
+    def test_analog(self):
+        wp = [20, 50]
+        ws = [10, 60]
+        rp = 3
+        rs = 80
+        N, Wn = cheb2ord(wp, ws, rp, rs, True)
+        b, a = cheby2(N, rs, Wn, 'bp', True)
+        w, h = freqs(b, a)
+        assert_array_less(-rp - 0.1,
+                          dB(h[np.logical_and(wp[0] <= w, w <= wp[1])]))
+        assert_array_less(dB(h[np.logical_or(w <= ws[0], ws[1] <= w)]),
+                          -rs + 0.1)
+
+        assert_equal(N, 11)
+        assert_allclose(Wn, [1.673740595370124e+01, 5.974641487254268e+01],
+                        rtol=1e-15)
+
+    def test_fs_param(self):
+        wp = 150
+        ws = 100
+        rp = 3
+        rs = 70
+        fs = 1000
+        N, Wn = cheb2ord(wp, ws, rp, rs, False, fs=fs)
+        b, a = cheby2(N, rs, Wn, 'hp', False, fs=fs)
+        w, h = freqz(b, a, fs=fs)
+        assert_array_less(-rp - 0.1, dB(h[wp <= w]))
+        assert_array_less(dB(h[w <= ws]), -rs + 0.1)
+
+        assert_equal(N, 9)
+        assert_allclose(Wn, 103.4874609145164, rtol=1e-15)
+
+    def test_invalid_input(self):
+        with pytest.raises(ValueError) as exc_info:
+            cheb2ord([0.1, 0.6], [0.2, 0.5], 3, 2)
+        assert "gpass should be smaller than gstop" in str(exc_info.value)
+
+        with pytest.raises(ValueError) as exc_info:
+            cheb2ord([0.1, 0.6], [0.2, 0.5], -1, 2)
+        assert "gpass should be larger than 0.0" in str(exc_info.value)
+
+        with pytest.raises(ValueError) as exc_info:
+            cheb2ord([0.1, 0.6], [0.2, 0.5], 1, -2)
+        assert "gstop should be larger than 0.0" in str(exc_info.value)
+
+    def test_ellip_cheb2(self):
+        # The purpose of the test is to compare to some known output from past
+        # scipy versions. The values to compare to are generated with scipy
+        # 1.9.1 (there is nothing special about this particular version though)
+        n, wn = cheb2ord([0.1, 0.6], [0.2, 0.5], 3, 60)
+        assert n == 7
+
+        n1, w1 = cheb1ord([0.1, 0.6], [0.2, 0.5], 3, 60)
+        assert not (wn == w1).all()
+
+    def test_fs_validation(self):
+        wp = 0.2
+        ws = 0.3
+        rp = 3
+        rs = 60
+
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            cheb2ord(wp, ws, rp, rs, False, fs=np.array([10, 20]))
+
+
+class TestEllipord:
+
+    def test_lowpass(self):
+        wp = 0.2
+        ws = 0.3
+        rp = 3
+        rs = 60
+        N, Wn = ellipord(wp, ws, rp, rs, False)
+        b, a = ellip(N, rp, rs, Wn, 'lp', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1, dB(h[w <= wp]))
+        assert_array_less(dB(h[ws <= w]), -rs + 0.1)
+
+        assert_equal(N, 5)
+        assert_allclose(Wn, 0.2, rtol=1e-15)
+
+    def test_lowpass_1000dB(self):
+        # failed when ellipkm1 wasn't used in ellipord and ellipap
+        wp = 0.2
+        ws = 0.3
+        rp = 3
+        rs = 1000
+        N, Wn = ellipord(wp, ws, rp, rs, False)
+        sos = ellip(N, rp, rs, Wn, 'lp', False, output='sos')
+        w, h = sosfreqz(sos)
+        w /= np.pi
+        assert_array_less(-rp - 0.1, dB(h[w <= wp]))
+        assert_array_less(dB(h[ws <= w]), -rs + 0.1)
+
+    def test_highpass(self):
+        wp = 0.3
+        ws = 0.2
+        rp = 3
+        rs = 70
+        N, Wn = ellipord(wp, ws, rp, rs, False)
+        b, a = ellip(N, rp, rs, Wn, 'hp', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1, dB(h[wp <= w]))
+        assert_array_less(dB(h[w <= ws]), -rs + 0.1)
+
+        assert_equal(N, 6)
+        assert_allclose(Wn, 0.3, rtol=1e-15)
+
+    def test_bandpass(self):
+        wp = [0.2, 0.5]
+        ws = [0.1, 0.6]
+        rp = 3
+        rs = 80
+        N, Wn = ellipord(wp, ws, rp, rs, False)
+        b, a = ellip(N, rp, rs, Wn, 'bp', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1,
+                          dB(h[np.logical_and(wp[0] <= w, w <= wp[1])]))
+        assert_array_less(dB(h[np.logical_or(w <= ws[0], ws[1] <= w)]),
+                          -rs + 0.1)
+
+        assert_equal(N, 6)
+        assert_allclose(Wn, [0.2, 0.5], rtol=1e-15)
+
+    def test_bandstop(self):
+        wp = [0.1, 0.6]
+        ws = [0.2, 0.5]
+        rp = 3
+        rs = 90
+        N, Wn = ellipord(wp, ws, rp, rs, False)
+        b, a = ellip(N, rp, rs, Wn, 'bs', False)
+        w, h = freqz(b, a)
+        w /= np.pi
+        assert_array_less(-rp - 0.1,
+                          dB(h[np.logical_or(w <= wp[0], wp[1] <= w)]))
+        assert_array_less(dB(h[np.logical_and(ws[0] <= w, w <= ws[1])]),
+                          -rs + 0.1)
+
+        assert_equal(N, 7)
+        assert_allclose(Wn, [0.14758232794342988, 0.6], rtol=1e-5)
+
+    def test_analog(self):
+        wp = [1000, 6000]
+        ws = [2000, 5000]
+        rp = 3
+        rs = 90
+        N, Wn = ellipord(wp, ws, rp, rs, True)
+        b, a = ellip(N, rp, rs, Wn, 'bs', True)
+        w, h = freqs(b, a)
+        assert_array_less(-rp - 0.1,
+                          dB(h[np.logical_or(w <= wp[0], wp[1] <= w)]))
+        assert_array_less(dB(h[np.logical_and(ws[0] <= w, w <= ws[1])]),
+                          -rs + 0.1)
+
+        assert_equal(N, 8)
+        assert_allclose(Wn, [1666.6666, 6000])
+
+        assert_equal(ellipord(1, 1.2, 1, 80, analog=True)[0], 9)
+
+    def test_fs_param(self):
+        wp = [400, 2400]
+        ws = [800, 2000]
+        rp = 3
+        rs = 90
+        fs = 8000
+        N, Wn = ellipord(wp, ws, rp, rs, False, fs=fs)
+        b, a = ellip(N, rp, rs, Wn, 'bs', False, fs=fs)
+        w, h = freqz(b, a, fs=fs)
+        assert_array_less(-rp - 0.1,
+                          dB(h[np.logical_or(w <= wp[0], wp[1] <= w)]))
+        assert_array_less(dB(h[np.logical_and(ws[0] <= w, w <= ws[1])]),
+                          -rs + 0.1)
+
+        assert_equal(N, 7)
+        assert_allclose(Wn, [590.3293117737195, 2400], rtol=1e-5)
+
+    def test_invalid_input(self):
+        with pytest.raises(ValueError) as exc_info:
+            ellipord(0.2, 0.5, 3, 2)
+        assert "gpass should be smaller than gstop" in str(exc_info.value)
+
+        with pytest.raises(ValueError) as exc_info:
+            ellipord(0.2, 0.5, -1, 2)
+        assert "gpass should be larger than 0.0" in str(exc_info.value)
+
+        with pytest.raises(ValueError) as exc_info:
+            ellipord(0.2, 0.5, 1, -2)
+        assert "gstop should be larger than 0.0" in str(exc_info.value)
+
+    def test_ellip_butter(self):
+        # The purpose of the test is to compare to some known output from past
+        # scipy versions. The values to compare to are generated with scipy
+        # 1.9.1 (there is nothing special about this particular version though)
+        n, wn = ellipord([0.1, 0.6], [0.2, 0.5], 3, 60)
+        assert n == 5
+
+    def test_fs_validation(self):
+        wp = 0.2
+        ws = 0.3
+        rp = 3
+        rs = 60
+
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            ellipord(wp, ws, rp, rs, False, fs=np.array([10, 20]))
+
+
+class TestBessel:
+
+    def test_degenerate(self):
+        for norm in ('delay', 'phase', 'mag'):
+            # 0-order filter is just a passthrough
+            b, a = bessel(0, 1, analog=True, norm=norm)
+            assert_array_equal(b, [1])
+            assert_array_equal(a, [1])
+
+            # 1-order filter is same for all types
+            b, a = bessel(1, 1, analog=True, norm=norm)
+            assert_allclose(b, [1], rtol=1e-15)
+            assert_allclose(a, [1, 1], rtol=1e-15)
+
+            z, p, k = bessel(1, 0.3, analog=True, output='zpk', norm=norm)
+            assert_array_equal(z, [])
+            assert_allclose(p, [-0.3], rtol=1e-14)
+            assert_allclose(k, 0.3, rtol=1e-14)
+
+    def test_high_order(self):
+        # high even order, 'phase'
+        z, p, k = bessel(24, 100, analog=True, output='zpk')
+        z2 = []
+        p2 = [
+             -9.055312334014323e+01 + 4.844005815403969e+00j,
+             -8.983105162681878e+01 + 1.454056170018573e+01j,
+             -8.837357994162065e+01 + 2.426335240122282e+01j,
+             -8.615278316179575e+01 + 3.403202098404543e+01j,
+             -8.312326467067703e+01 + 4.386985940217900e+01j,
+             -7.921695461084202e+01 + 5.380628489700191e+01j,
+             -7.433392285433246e+01 + 6.388084216250878e+01j,
+             -6.832565803501586e+01 + 7.415032695116071e+01j,
+             -6.096221567378025e+01 + 8.470292433074425e+01j,
+             -5.185914574820616e+01 + 9.569048385258847e+01j,
+             -4.027853855197555e+01 + 1.074195196518679e+02j,
+             -2.433481337524861e+01 + 1.207298683731973e+02j,
+             ]
+        k2 = 9.999999999999989e+47
+        assert_array_equal(z, z2)
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(np.union1d(p2, np.conj(p2)), key=np.imag))
+        assert_allclose(k, k2, rtol=1e-14)
+
+        # high odd order, 'phase'
+        z, p, k = bessel(23, 1000, analog=True, output='zpk')
+        z2 = []
+        p2 = [
+             -2.497697202208956e+02 + 1.202813187870698e+03j,
+             -4.126986617510172e+02 + 1.065328794475509e+03j,
+             -5.304922463809596e+02 + 9.439760364018479e+02j,
+             -9.027564978975828e+02 + 1.010534334242318e+02j,
+             -8.909283244406079e+02 + 2.023024699647598e+02j,
+             -8.709469394347836e+02 + 3.039581994804637e+02j,
+             -8.423805948131370e+02 + 4.062657947488952e+02j,
+             -8.045561642249877e+02 + 5.095305912401127e+02j,
+             -7.564660146766259e+02 + 6.141594859516342e+02j,
+             -6.965966033906477e+02 + 7.207341374730186e+02j,
+             -6.225903228776276e+02 + 8.301558302815096e+02j,
+             -9.066732476324988e+02]
+        k2 = 9.999999999999983e+68
+        assert_array_equal(z, z2)
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(np.union1d(p2, np.conj(p2)), key=np.imag))
+        assert_allclose(k, k2, rtol=1e-14)
+
+        # high even order, 'delay' (Orchard 1965 "The Roots of the
+        # Maximally Flat-Delay Polynomials" Table 1)
+        z, p, k = bessel(31, 1, analog=True, output='zpk', norm='delay')
+        p2 = [-20.876706,
+              -20.826543 + 1.735732j,
+              -20.675502 + 3.473320j,
+              -20.421895 + 5.214702j,
+              -20.062802 + 6.961982j,
+              -19.593895 + 8.717546j,
+              -19.009148 + 10.484195j,
+              -18.300400 + 12.265351j,
+              -17.456663 + 14.065350j,
+              -16.463032 + 15.889910j,
+              -15.298849 + 17.746914j,
+              -13.934466 + 19.647827j,
+              -12.324914 + 21.610519j,
+              -10.395893 + 23.665701j,
+              - 8.005600 + 25.875019j,
+              - 4.792045 + 28.406037j,
+              ]
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(np.union1d(p2, np.conj(p2)), key=np.imag))
+
+        # high odd order, 'delay'
+        z, p, k = bessel(30, 1, analog=True, output='zpk', norm='delay')
+        p2 = [-20.201029 + 0.867750j,
+              -20.097257 + 2.604235j,
+              -19.888485 + 4.343721j,
+              -19.572188 + 6.088363j,
+              -19.144380 + 7.840570j,
+              -18.599342 + 9.603147j,
+              -17.929195 + 11.379494j,
+              -17.123228 + 13.173901j,
+              -16.166808 + 14.992008j,
+              -15.039580 + 16.841580j,
+              -13.712245 + 18.733902j,
+              -12.140295 + 20.686563j,
+              -10.250119 + 22.729808j,
+              - 7.901170 + 24.924391j,
+              - 4.734679 + 27.435615j,
+              ]
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(np.union1d(p2, np.conj(p2)), key=np.imag))
+
+    def test_refs(self):
+        # Compare to http://www.crbond.com/papers/bsf2.pdf
+        # "Delay Normalized Bessel Polynomial Coefficients"
+        bond_b = 10395
+        bond_a = [1, 21, 210, 1260, 4725, 10395, 10395]
+        b, a = bessel(6, 1, norm='delay', analog=True)
+        assert_allclose(bond_b, b)
+        assert_allclose(bond_a, a)
+
+        # "Delay Normalized Bessel Pole Locations"
+        bond_poles = {
+            1: [-1.0000000000],
+            2: [-1.5000000000 + 0.8660254038j],
+            3: [-1.8389073227 + 1.7543809598j, -2.3221853546],
+            4: [-2.1037893972 + 2.6574180419j, -2.8962106028 + 0.8672341289j],
+            5: [-2.3246743032 + 3.5710229203j, -3.3519563992 + 1.7426614162j,
+                -3.6467385953],
+            6: [-2.5159322478 + 4.4926729537j, -3.7357083563 + 2.6262723114j,
+                -4.2483593959 + 0.8675096732j],
+            7: [-2.6856768789 + 5.4206941307j, -4.0701391636 + 3.5171740477j,
+                -4.7582905282 + 1.7392860611j, -4.9717868585],
+            8: [-2.8389839489 + 6.3539112986j, -4.3682892172 + 4.4144425005j,
+                -5.2048407906 + 2.6161751526j, -5.5878860433 + 0.8676144454j],
+            9: [-2.9792607982 + 7.2914636883j, -4.6384398872 + 5.3172716754j,
+                -5.6044218195 + 3.4981569179j, -6.1293679043 + 1.7378483835j,
+                -6.2970191817],
+            10: [-3.1089162336 + 8.2326994591j, -4.8862195669 + 6.2249854825j,
+                 -5.9675283286 + 4.3849471889j, -6.6152909655 + 2.6115679208j,
+                 -6.9220449054 + 0.8676651955j]
+            }
+
+        for N in range(1, 11):
+            p1 = np.sort(bond_poles[N])
+            p2 = np.sort(np.concatenate(_cplxreal(besselap(N, 'delay')[1])))
+            assert_array_almost_equal(p1, p2, decimal=10)
+
+        # "Frequency Normalized Bessel Pole Locations"
+        bond_poles = {
+            1: [-1.0000000000],
+            2: [-1.1016013306 + 0.6360098248j],
+            3: [-1.0474091610 + 0.9992644363j, -1.3226757999],
+            4: [-0.9952087644 + 1.2571057395j, -1.3700678306 + 0.4102497175j],
+            5: [-0.9576765486 + 1.4711243207j, -1.3808773259 + 0.7179095876j,
+                -1.5023162714],
+            6: [-0.9306565229 + 1.6618632689j, -1.3818580976 + 0.9714718907j,
+                -1.5714904036 + 0.3208963742j],
+            7: [-0.9098677806 + 1.8364513530j, -1.3789032168 + 1.1915667778j,
+                -1.6120387662 + 0.5892445069j, -1.6843681793],
+            8: [-0.8928697188 + 1.9983258436j, -1.3738412176 + 1.3883565759j,
+                -1.6369394181 + 0.8227956251j, -1.7574084004 + 0.2728675751j],
+            9: [-0.8783992762 + 2.1498005243j, -1.3675883098 + 1.5677337122j,
+                -1.6523964846 + 1.0313895670j, -1.8071705350 + 0.5123837306j,
+                -1.8566005012],
+            10: [-0.8657569017 + 2.2926048310j, -1.3606922784 + 1.7335057427j,
+                 -1.6618102414 + 1.2211002186j, -1.8421962445 + 0.7272575978j,
+                 -1.9276196914 + 0.2416234710j]
+            }
+
+        for N in range(1, 11):
+            p1 = np.sort(bond_poles[N])
+            p2 = np.sort(np.concatenate(_cplxreal(besselap(N, 'mag')[1])))
+            assert_array_almost_equal(p1, p2, decimal=10)
+
+        # Compare to https://www.ranecommercial.com/legacy/note147.html
+        # "Table 1 - Bessel Crossovers of Second, Third, and Fourth-Order"
+        a = [1, 1, 1/3]
+        b2, a2 = bessel(2, 1, norm='delay', analog=True)
+        assert_allclose(a[::-1], a2/b2)
+
+        a = [1, 1, 2/5, 1/15]
+        b2, a2 = bessel(3, 1, norm='delay', analog=True)
+        assert_allclose(a[::-1], a2/b2)
+
+        a = [1, 1, 9/21, 2/21, 1/105]
+        b2, a2 = bessel(4, 1, norm='delay', analog=True)
+        assert_allclose(a[::-1], a2/b2)
+
+        a = [1, np.sqrt(3), 1]
+        b2, a2 = bessel(2, 1, norm='phase', analog=True)
+        assert_allclose(a[::-1], a2/b2)
+
+        # TODO: Why so inaccurate?  Is reference flawed?
+        a = [1, 2.481, 2.463, 1.018]
+        b2, a2 = bessel(3, 1, norm='phase', analog=True)
+        assert_array_almost_equal(a[::-1], a2/b2, decimal=1)
+
+        # TODO: Why so inaccurate?  Is reference flawed?
+        a = [1, 3.240, 4.5, 3.240, 1.050]
+        b2, a2 = bessel(4, 1, norm='phase', analog=True)
+        assert_array_almost_equal(a[::-1], a2/b2, decimal=1)
+
+        # Table of -3 dB factors:
+        N, scale = 2, 1.272
+        scale2 = besselap(N, 'mag')[1] / besselap(N, 'phase')[1]
+        assert_array_almost_equal(scale, scale2, decimal=3)
+
+        # TODO: Why so inaccurate?  Is reference flawed?
+        N, scale = 3, 1.413
+        scale2 = besselap(N, 'mag')[1] / besselap(N, 'phase')[1]
+        assert_array_almost_equal(scale, scale2, decimal=2)
+
+        # TODO: Why so inaccurate?  Is reference flawed?
+        N, scale = 4, 1.533
+        scale2 = besselap(N, 'mag')[1] / besselap(N, 'phase')[1]
+        assert_array_almost_equal(scale, scale2, decimal=1)
+
+    def test_hardcoded(self):
+        # Compare to values from original hardcoded implementation
+        originals = {
+            0: [],
+            1: [-1],
+            2: [-.8660254037844386467637229 + .4999999999999999999999996j],
+            3: [-.9416000265332067855971980,
+                -.7456403858480766441810907 + .7113666249728352680992154j],
+            4: [-.6572111716718829545787788 + .8301614350048733772399715j,
+                -.9047587967882449459642624 + .2709187330038746636700926j],
+            5: [-.9264420773877602247196260,
+                -.8515536193688395541722677 + .4427174639443327209850002j,
+                -.5905759446119191779319432 + .9072067564574549539291747j],
+            6: [-.9093906830472271808050953 + .1856964396793046769246397j,
+                -.7996541858328288520243325 + .5621717346937317988594118j,
+                -.5385526816693109683073792 + .9616876881954277199245657j],
+            7: [-.9194871556490290014311619,
+                -.8800029341523374639772340 + .3216652762307739398381830j,
+                -.7527355434093214462291616 + .6504696305522550699212995j,
+                -.4966917256672316755024763 + 1.002508508454420401230220j],
+            8: [-.9096831546652910216327629 + .1412437976671422927888150j,
+                -.8473250802359334320103023 + .4259017538272934994996429j,
+                -.7111381808485399250796172 + .7186517314108401705762571j,
+                -.4621740412532122027072175 + 1.034388681126901058116589j],
+            9: [-.9154957797499037686769223,
+                -.8911217017079759323183848 + .2526580934582164192308115j,
+                -.8148021112269012975514135 + .5085815689631499483745341j,
+                -.6743622686854761980403401 + .7730546212691183706919682j,
+                -.4331415561553618854685942 + 1.060073670135929666774323j],
+            10: [-.9091347320900502436826431 + .1139583137335511169927714j,
+                 -.8688459641284764527921864 + .3430008233766309973110589j,
+                 -.7837694413101441082655890 + .5759147538499947070009852j,
+                 -.6417513866988316136190854 + .8175836167191017226233947j,
+                 -.4083220732868861566219785 + 1.081274842819124562037210j],
+            11: [-.9129067244518981934637318,
+                 -.8963656705721166099815744 + .2080480375071031919692341j,
+                 -.8453044014712962954184557 + .4178696917801248292797448j,
+                 -.7546938934722303128102142 + .6319150050721846494520941j,
+                 -.6126871554915194054182909 + .8547813893314764631518509j,
+                 -.3868149510055090879155425 + 1.099117466763120928733632j],
+            12: [-.9084478234140682638817772 + 95506365213450398415258360e-27j,
+                 -.8802534342016826507901575 + .2871779503524226723615457j,
+                 -.8217296939939077285792834 + .4810212115100676440620548j,
+                 -.7276681615395159454547013 + .6792961178764694160048987j,
+                 -.5866369321861477207528215 + .8863772751320727026622149j,
+                 -.3679640085526312839425808 + 1.114373575641546257595657j],
+            13: [-.9110914665984182781070663,
+                 -.8991314665475196220910718 + .1768342956161043620980863j,
+                 -.8625094198260548711573628 + .3547413731172988997754038j,
+                 -.7987460692470972510394686 + .5350752120696801938272504j,
+                 -.7026234675721275653944062 + .7199611890171304131266374j,
+                 -.5631559842430199266325818 + .9135900338325109684927731j,
+                 -.3512792323389821669401925 + 1.127591548317705678613239j],
+            14: [-.9077932138396487614720659 + 82196399419401501888968130e-27j,
+                 -.8869506674916445312089167 + .2470079178765333183201435j,
+                 -.8441199160909851197897667 + .4131653825102692595237260j,
+                 -.7766591387063623897344648 + .5819170677377608590492434j,
+                 -.6794256425119233117869491 + .7552857305042033418417492j,
+                 -.5418766775112297376541293 + .9373043683516919569183099j,
+                 -.3363868224902037330610040 + 1.139172297839859991370924j],
+            15: [-.9097482363849064167228581,
+                 -.9006981694176978324932918 + .1537681197278439351298882j,
+                 -.8731264620834984978337843 + .3082352470564267657715883j,
+                 -.8256631452587146506294553 + .4642348752734325631275134j,
+                 -.7556027168970728127850416 + .6229396358758267198938604j,
+                 -.6579196593110998676999362 + .7862895503722515897065645j,
+                 -.5224954069658330616875186 + .9581787261092526478889345j,
+                 -.3229963059766444287113517 + 1.149416154583629539665297j],
+            16: [-.9072099595087001356491337 + 72142113041117326028823950e-27j,
+                 -.8911723070323647674780132 + .2167089659900576449410059j,
+                 -.8584264231521330481755780 + .3621697271802065647661080j,
+                 -.8074790293236003885306146 + .5092933751171800179676218j,
+                 -.7356166304713115980927279 + .6591950877860393745845254j,
+                 -.6379502514039066715773828 + .8137453537108761895522580j,
+                 -.5047606444424766743309967 + .9767137477799090692947061j,
+                 -.3108782755645387813283867 + 1.158552841199330479412225j],
+            17: [-.9087141161336397432860029,
+                 -.9016273850787285964692844 + .1360267995173024591237303j,
+                 -.8801100704438627158492165 + .2725347156478803885651973j,
+                 -.8433414495836129204455491 + .4100759282910021624185986j,
+                 -.7897644147799708220288138 + .5493724405281088674296232j,
+                 -.7166893842372349049842743 + .6914936286393609433305754j,
+                 -.6193710717342144521602448 + .8382497252826992979368621j,
+                 -.4884629337672704194973683 + .9932971956316781632345466j,
+                 -.2998489459990082015466971 + 1.166761272925668786676672j],
+            18: [-.9067004324162775554189031 + 64279241063930693839360680e-27j,
+                 -.8939764278132455733032155 + .1930374640894758606940586j,
+                 -.8681095503628830078317207 + .3224204925163257604931634j,
+                 -.8281885016242836608829018 + .4529385697815916950149364j,
+                 -.7726285030739558780127746 + .5852778162086640620016316j,
+                 -.6987821445005273020051878 + .7204696509726630531663123j,
+                 -.6020482668090644386627299 + .8602708961893664447167418j,
+                 -.4734268069916151511140032 + 1.008234300314801077034158j,
+                 -.2897592029880489845789953 + 1.174183010600059128532230j],
+            19: [-.9078934217899404528985092,
+                 -.9021937639390660668922536 + .1219568381872026517578164j,
+                 -.8849290585034385274001112 + .2442590757549818229026280j,
+                 -.8555768765618421591093993 + .3672925896399872304734923j,
+                 -.8131725551578197705476160 + .4915365035562459055630005j,
+                 -.7561260971541629355231897 + .6176483917970178919174173j,
+                 -.6818424412912442033411634 + .7466272357947761283262338j,
+                 -.5858613321217832644813602 + .8801817131014566284786759j,
+                 -.4595043449730988600785456 + 1.021768776912671221830298j,
+                 -.2804866851439370027628724 + 1.180931628453291873626003j],
+            20: [-.9062570115576771146523497 + 57961780277849516990208850e-27j,
+                 -.8959150941925768608568248 + .1740317175918705058595844j,
+                 -.8749560316673332850673214 + .2905559296567908031706902j,
+                 -.8427907479956670633544106 + .4078917326291934082132821j,
+                 -.7984251191290606875799876 + .5264942388817132427317659j,
+                 -.7402780309646768991232610 + .6469975237605228320268752j,
+                 -.6658120544829934193890626 + .7703721701100763015154510j,
+                 -.5707026806915714094398061 + .8982829066468255593407161j,
+                 -.4465700698205149555701841 + 1.034097702560842962315411j,
+                 -.2719299580251652601727704 + 1.187099379810885886139638j],
+            21: [-.9072262653142957028884077,
+                 -.9025428073192696303995083 + .1105252572789856480992275j,
+                 -.8883808106664449854431605 + .2213069215084350419975358j,
+                 -.8643915813643204553970169 + .3326258512522187083009453j,
+                 -.8299435470674444100273463 + .4448177739407956609694059j,
+                 -.7840287980408341576100581 + .5583186348022854707564856j,
+                 -.7250839687106612822281339 + .6737426063024382240549898j,
+                 -.6506315378609463397807996 + .7920349342629491368548074j,
+                 -.5564766488918562465935297 + .9148198405846724121600860j,
+                 -.4345168906815271799687308 + 1.045382255856986531461592j,
+                 -.2640041595834031147954813 + 1.192762031948052470183960j],
+            22: [-.9058702269930872551848625 + 52774908289999045189007100e-27j,
+                 -.8972983138153530955952835 + .1584351912289865608659759j,
+                 -.8799661455640176154025352 + .2644363039201535049656450j,
+                 -.8534754036851687233084587 + .3710389319482319823405321j,
+                 -.8171682088462720394344996 + .4785619492202780899653575j,
+                 -.7700332930556816872932937 + .5874255426351153211965601j,
+                 -.7105305456418785989070935 + .6982266265924524000098548j,
+                 -.6362427683267827226840153 + .8118875040246347267248508j,
+                 -.5430983056306302779658129 + .9299947824439872998916657j,
+                 -.4232528745642628461715044 + 1.055755605227545931204656j,
+                 -.2566376987939318038016012 + 1.197982433555213008346532j],
+            23: [-.9066732476324988168207439,
+                 -.9027564979912504609412993 + .1010534335314045013252480j,
+                 -.8909283242471251458653994 + .2023024699381223418195228j,
+                 -.8709469395587416239596874 + .3039581993950041588888925j,
+                 -.8423805948021127057054288 + .4062657948237602726779246j,
+                 -.8045561642053176205623187 + .5095305912227258268309528j,
+                 -.7564660146829880581478138 + .6141594859476032127216463j,
+                 -.6965966033912705387505040 + .7207341374753046970247055j,
+                 -.6225903228771341778273152 + .8301558302812980678845563j,
+                 -.5304922463810191698502226 + .9439760364018300083750242j,
+                 -.4126986617510148836149955 + 1.065328794475513585531053j,
+                 -.2497697202208956030229911 + 1.202813187870697831365338j],
+            24: [-.9055312363372773709269407 + 48440066540478700874836350e-27j,
+                 -.8983105104397872954053307 + .1454056133873610120105857j,
+                 -.8837358034555706623131950 + .2426335234401383076544239j,
+                 -.8615278304016353651120610 + .3403202112618624773397257j,
+                 -.8312326466813240652679563 + .4386985933597305434577492j,
+                 -.7921695462343492518845446 + .5380628490968016700338001j,
+                 -.7433392285088529449175873 + .6388084216222567930378296j,
+                 -.6832565803536521302816011 + .7415032695091650806797753j,
+                 -.6096221567378335562589532 + .8470292433077202380020454j,
+                 -.5185914574820317343536707 + .9569048385259054576937721j,
+                 -.4027853855197518014786978 + 1.074195196518674765143729j,
+                 -.2433481337524869675825448 + 1.207298683731972524975429j],
+            25: [-.9062073871811708652496104,
+                 -.9028833390228020537142561 + 93077131185102967450643820e-27j,
+                 -.8928551459883548836774529 + .1863068969804300712287138j,
+                 -.8759497989677857803656239 + .2798521321771408719327250j,
+                 -.8518616886554019782346493 + .3738977875907595009446142j,
+                 -.8201226043936880253962552 + .4686668574656966589020580j,
+                 -.7800496278186497225905443 + .5644441210349710332887354j,
+                 -.7306549271849967721596735 + .6616149647357748681460822j,
+                 -.6704827128029559528610523 + .7607348858167839877987008j,
+                 -.5972898661335557242320528 + .8626676330388028512598538j,
+                 -.5073362861078468845461362 + .9689006305344868494672405j,
+                 -.3934529878191079606023847 + 1.082433927173831581956863j,
+                 -.2373280669322028974199184 + 1.211476658382565356579418j],
+            }
+        for N in originals:
+            p1 = sorted(np.union1d(originals[N],
+                                   np.conj(originals[N])), key=np.imag)
+            p2 = sorted(besselap(N)[1], key=np.imag)
+            assert_allclose(p1, p2, rtol=1e-14)
+
+    def test_norm_phase(self):
+        # Test some orders and frequencies and see that they have the right
+        # phase at w0
+        for N in (1, 2, 3, 4, 5, 51, 72):
+            for w0 in (1, 100):
+                b, a = bessel(N, w0, analog=True, norm='phase')
+                w = np.linspace(0, w0, 100)
+                w, h = freqs(b, a, w)
+                phase = np.unwrap(np.angle(h))
+                assert_allclose(phase[[0, -1]], (0, -N*pi/4), rtol=1e-1)
+
+    def test_norm_mag(self):
+        # Test some orders and frequencies and see that they have the right
+        # mag at w0
+        for N in (1, 2, 3, 4, 5, 51, 72):
+            for w0 in (1, 100):
+                b, a = bessel(N, w0, analog=True, norm='mag')
+                w = (0, w0)
+                w, h = freqs(b, a, w)
+                mag = abs(h)
+                assert_allclose(mag, (1, 1/np.sqrt(2)))
+
+    def test_norm_delay(self):
+        # Test some orders and frequencies and see that they have the right
+        # delay at DC
+        for N in (1, 2, 3, 4, 5, 51, 72):
+            for w0 in (1, 100):
+                b, a = bessel(N, w0, analog=True, norm='delay')
+                w = np.linspace(0, 10*w0, 1000)
+                w, h = freqs(b, a, w)
+                delay = -np.diff(np.unwrap(np.angle(h)))/np.diff(w)
+                assert_allclose(delay[0], 1/w0, rtol=1e-4)
+
+    def test_norm_factor(self):
+        mpmath_values = {
+            1: 1, 2: 1.361654128716130520, 3: 1.755672368681210649,
+            4: 2.113917674904215843, 5: 2.427410702152628137,
+            6: 2.703395061202921876, 7: 2.951722147038722771,
+            8: 3.179617237510651330, 9: 3.391693138911660101,
+            10: 3.590980594569163482, 11: 3.779607416439620092,
+            12: 3.959150821144285315, 13: 4.130825499383535980,
+            14: 4.295593409533637564, 15: 4.454233021624377494,
+            16: 4.607385465472647917, 17: 4.755586548961147727,
+            18: 4.899289677284488007, 19: 5.038882681488207605,
+            20: 5.174700441742707423, 21: 5.307034531360917274,
+            22: 5.436140703250035999, 23: 5.562244783787878196,
+            24: 5.685547371295963521, 25: 5.806227623775418541,
+            50: 8.268963160013226298, 51: 8.352374541546012058,
+            }
+        for N in mpmath_values:
+            z, p, k = besselap(N, 'delay')
+            assert_allclose(mpmath_values[N], _norm_factor(p, k), rtol=1e-13)
+
+    def test_bessel_poly(self):
+        assert_array_equal(_bessel_poly(5), [945, 945, 420, 105, 15, 1])
+        assert_array_equal(_bessel_poly(4, True), [1, 10, 45, 105, 105])
+
+    def test_bessel_zeros(self):
+        assert_array_equal(_bessel_zeros(0), [])
+
+    def test_invalid(self):
+        assert_raises(ValueError, besselap, 5, 'nonsense')
+        assert_raises(ValueError, besselap, -5)
+        assert_raises(ValueError, besselap, 3.2)
+        assert_raises(ValueError, _bessel_poly, -3)
+        assert_raises(ValueError, _bessel_poly, 3.3)
+
+    @pytest.mark.fail_slow(5)
+    def test_fs_param(self):
+        for norm in ('phase', 'mag', 'delay'):
+            for fs in (900, 900.1, 1234.567):
+                for N in (0, 1, 2, 3, 10):
+                    for fc in (100, 100.1, 432.12345):
+                        for btype in ('lp', 'hp'):
+                            ba1 = bessel(N, fc, btype, norm=norm, fs=fs)
+                            ba2 = bessel(N, fc/(fs/2), btype, norm=norm)
+                            assert_allclose(ba1, ba2)
+                    for fc in ((100, 200), (100.1, 200.2), (321.123, 432.123)):
+                        for btype in ('bp', 'bs'):
+                            ba1 = bessel(N, fc, btype, norm=norm, fs=fs)
+                            for seq in (list, tuple, array):
+                                fcnorm = seq([f/(fs/2) for f in fc])
+                                ba2 = bessel(N, fcnorm, btype, norm=norm)
+                                assert_allclose(ba1, ba2)
+
+
+class TestButter:
+
+    def test_degenerate(self):
+        # 0-order filter is just a passthrough
+        b, a = butter(0, 1, analog=True)
+        assert_array_equal(b, [1])
+        assert_array_equal(a, [1])
+
+        # 1-order filter is same for all types
+        b, a = butter(1, 1, analog=True)
+        assert_array_almost_equal(b, [1])
+        assert_array_almost_equal(a, [1, 1])
+
+        z, p, k = butter(1, 0.3, output='zpk')
+        assert_array_equal(z, [-1])
+        assert_allclose(p, [3.249196962329063e-01], rtol=1e-14)
+        assert_allclose(k, 3.375401518835469e-01, rtol=1e-14)
+
+    def test_basic(self):
+        # analog s-plane
+        for N in range(25):
+            wn = 0.01
+            z, p, k = butter(N, wn, 'low', analog=True, output='zpk')
+            assert_array_almost_equal([], z)
+            assert_(len(p) == N)
+            # All poles should be at distance wn from origin
+            assert_array_almost_equal(wn, abs(p))
+            assert_(all(np.real(p) <= 0))  # No poles in right half of S-plane
+            assert_array_almost_equal(wn**N, k)
+
+        # digital z-plane
+        for N in range(25):
+            wn = 0.01
+            z, p, k = butter(N, wn, 'high', analog=False, output='zpk')
+            assert_array_equal(np.ones(N), z)  # All zeros exactly at DC
+            assert_(all(np.abs(p) <= 1))  # No poles outside unit circle
+
+        b1, a1 = butter(2, 1, analog=True)
+        assert_array_almost_equal(b1, [1])
+        assert_array_almost_equal(a1, [1, np.sqrt(2), 1])
+
+        b2, a2 = butter(5, 1, analog=True)
+        assert_array_almost_equal(b2, [1])
+        assert_array_almost_equal(a2, [1, 3.2361, 5.2361,
+                                       5.2361, 3.2361, 1], decimal=4)
+
+        b3, a3 = butter(10, 1, analog=True)
+        assert_array_almost_equal(b3, [1])
+        assert_array_almost_equal(a3, [1, 6.3925, 20.4317, 42.8021, 64.8824,
+                                       74.2334, 64.8824, 42.8021, 20.4317,
+                                       6.3925, 1], decimal=4)
+
+        b2, a2 = butter(19, 1.0441379169150726, analog=True)
+        assert_array_almost_equal(b2, [2.2720], decimal=4)
+        assert_array_almost_equal(a2, 1.0e+004 * np.array([
+                        0.0001, 0.0013, 0.0080, 0.0335, 0.1045, 0.2570,
+                        0.5164, 0.8669, 1.2338, 1.5010, 1.5672, 1.4044,
+                        1.0759, 0.6986, 0.3791, 0.1681, 0.0588, 0.0153,
+                        0.0026, 0.0002]), decimal=0)
+
+        b, a = butter(5, 0.4)
+        assert_array_almost_equal(b, [0.0219, 0.1097, 0.2194,
+                                      0.2194, 0.1097, 0.0219], decimal=4)
+        assert_array_almost_equal(a, [1.0000, -0.9853, 0.9738,
+                                      -0.3864, 0.1112, -0.0113], decimal=4)
+
+    def test_highpass(self):
+        # highpass, high even order
+        z, p, k = butter(28, 0.43, 'high', output='zpk')
+        z2 = np.ones(28)
+        p2 = [
+            2.068257195514592e-01 + 9.238294351481734e-01j,
+            2.068257195514592e-01 - 9.238294351481734e-01j,
+            1.874933103892023e-01 + 8.269455076775277e-01j,
+            1.874933103892023e-01 - 8.269455076775277e-01j,
+            1.717435567330153e-01 + 7.383078571194629e-01j,
+            1.717435567330153e-01 - 7.383078571194629e-01j,
+            1.588266870755982e-01 + 6.564623730651094e-01j,
+            1.588266870755982e-01 - 6.564623730651094e-01j,
+            1.481881532502603e-01 + 5.802343458081779e-01j,
+            1.481881532502603e-01 - 5.802343458081779e-01j,
+            1.394122576319697e-01 + 5.086609000582009e-01j,
+            1.394122576319697e-01 - 5.086609000582009e-01j,
+            1.321840881809715e-01 + 4.409411734716436e-01j,
+            1.321840881809715e-01 - 4.409411734716436e-01j,
+            1.262633413354405e-01 + 3.763990035551881e-01j,
+            1.262633413354405e-01 - 3.763990035551881e-01j,
+            1.214660449478046e-01 + 3.144545234797277e-01j,
+            1.214660449478046e-01 - 3.144545234797277e-01j,
+            1.104868766650320e-01 + 2.771505404367791e-02j,
+            1.104868766650320e-01 - 2.771505404367791e-02j,
+            1.111768629525075e-01 + 8.331369153155753e-02j,
+            1.111768629525075e-01 - 8.331369153155753e-02j,
+            1.125740630842972e-01 + 1.394219509611784e-01j,
+            1.125740630842972e-01 - 1.394219509611784e-01j,
+            1.147138487992747e-01 + 1.963932363793666e-01j,
+            1.147138487992747e-01 - 1.963932363793666e-01j,
+            1.176516491045901e-01 + 2.546021573417188e-01j,
+            1.176516491045901e-01 - 2.546021573417188e-01j,
+            ]
+        k2 = 1.446671081817286e-06
+        assert_array_equal(z, z2)
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(p2, key=np.imag), rtol=1e-7)
+        assert_allclose(k, k2, rtol=1e-10)
+
+        # highpass, high odd order
+        z, p, k = butter(27, 0.56, 'high', output='zpk')
+        z2 = np.ones(27)
+        p2 = [
+            -1.772572785680147e-01 + 9.276431102995948e-01j,
+            -1.772572785680147e-01 - 9.276431102995948e-01j,
+            -1.600766565322114e-01 + 8.264026279893268e-01j,
+            -1.600766565322114e-01 - 8.264026279893268e-01j,
+            -1.461948419016121e-01 + 7.341841939120078e-01j,
+            -1.461948419016121e-01 - 7.341841939120078e-01j,
+            -1.348975284762046e-01 + 6.493235066053785e-01j,
+            -1.348975284762046e-01 - 6.493235066053785e-01j,
+            -1.256628210712206e-01 + 5.704921366889227e-01j,
+            -1.256628210712206e-01 - 5.704921366889227e-01j,
+            -1.181038235962314e-01 + 4.966120551231630e-01j,
+            -1.181038235962314e-01 - 4.966120551231630e-01j,
+            -1.119304913239356e-01 + 4.267938916403775e-01j,
+            -1.119304913239356e-01 - 4.267938916403775e-01j,
+            -1.069237739782691e-01 + 3.602914879527338e-01j,
+            -1.069237739782691e-01 - 3.602914879527338e-01j,
+            -1.029178030691416e-01 + 2.964677964142126e-01j,
+            -1.029178030691416e-01 - 2.964677964142126e-01j,
+            -9.978747500816100e-02 + 2.347687643085738e-01j,
+            -9.978747500816100e-02 - 2.347687643085738e-01j,
+            -9.743974496324025e-02 + 1.747028739092479e-01j,
+            -9.743974496324025e-02 - 1.747028739092479e-01j,
+            -9.580754551625957e-02 + 1.158246860771989e-01j,
+            -9.580754551625957e-02 - 1.158246860771989e-01j,
+            -9.484562207782568e-02 + 5.772118357151691e-02j,
+            -9.484562207782568e-02 - 5.772118357151691e-02j,
+            -9.452783117928215e-02
+            ]
+        k2 = 9.585686688851069e-09
+        assert_array_equal(z, z2)
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(p2, key=np.imag), rtol=1e-8)
+        assert_allclose(k, k2)
+
+    def test_bandpass(self):
+        z, p, k = butter(8, [0.25, 0.33], 'band', output='zpk')
+        z2 = [1, 1, 1, 1, 1, 1, 1, 1,
+              -1, -1, -1, -1, -1, -1, -1, -1]
+        p2 = [
+            4.979909925436156e-01 + 8.367609424799387e-01j,
+            4.979909925436156e-01 - 8.367609424799387e-01j,
+            4.913338722555539e-01 + 7.866774509868817e-01j,
+            4.913338722555539e-01 - 7.866774509868817e-01j,
+            5.035229361778706e-01 + 7.401147376726750e-01j,
+            5.035229361778706e-01 - 7.401147376726750e-01j,
+            5.307617160406101e-01 + 7.029184459442954e-01j,
+            5.307617160406101e-01 - 7.029184459442954e-01j,
+            5.680556159453138e-01 + 6.788228792952775e-01j,
+            5.680556159453138e-01 - 6.788228792952775e-01j,
+            6.100962560818854e-01 + 6.693849403338664e-01j,
+            6.100962560818854e-01 - 6.693849403338664e-01j,
+            6.904694312740631e-01 + 6.930501690145245e-01j,
+            6.904694312740631e-01 - 6.930501690145245e-01j,
+            6.521767004237027e-01 + 6.744414640183752e-01j,
+            6.521767004237027e-01 - 6.744414640183752e-01j,
+            ]
+        k2 = 3.398854055800844e-08
+        assert_array_equal(z, z2)
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(p2, key=np.imag), rtol=1e-13)
+        assert_allclose(k, k2, rtol=1e-13)
+
+        # bandpass analog
+        z, p, k = butter(4, [90.5, 110.5], 'bp', analog=True, output='zpk')
+        z2 = np.zeros(4)
+        p2 = [
+            -4.179137760733086e+00 + 1.095935899082837e+02j,
+            -4.179137760733086e+00 - 1.095935899082837e+02j,
+            -9.593598668443835e+00 + 1.034745398029734e+02j,
+            -9.593598668443835e+00 - 1.034745398029734e+02j,
+            -8.883991981781929e+00 + 9.582087115567160e+01j,
+            -8.883991981781929e+00 - 9.582087115567160e+01j,
+            -3.474530886568715e+00 + 9.111599925805801e+01j,
+            -3.474530886568715e+00 - 9.111599925805801e+01j,
+            ]
+        k2 = 1.600000000000001e+05
+        assert_array_equal(z, z2)
+        assert_allclose(sorted(p, key=np.imag), sorted(p2, key=np.imag))
+        assert_allclose(k, k2, rtol=1e-15)
+
+    def test_bandstop(self):
+        z, p, k = butter(7, [0.45, 0.56], 'stop', output='zpk')
+        z2 = [-1.594474531383421e-02 + 9.998728744679880e-01j,
+              -1.594474531383421e-02 - 9.998728744679880e-01j,
+              -1.594474531383421e-02 + 9.998728744679880e-01j,
+              -1.594474531383421e-02 - 9.998728744679880e-01j,
+              -1.594474531383421e-02 + 9.998728744679880e-01j,
+              -1.594474531383421e-02 - 9.998728744679880e-01j,
+              -1.594474531383421e-02 + 9.998728744679880e-01j,
+              -1.594474531383421e-02 - 9.998728744679880e-01j,
+              -1.594474531383421e-02 + 9.998728744679880e-01j,
+              -1.594474531383421e-02 - 9.998728744679880e-01j,
+              -1.594474531383421e-02 + 9.998728744679880e-01j,
+              -1.594474531383421e-02 - 9.998728744679880e-01j,
+              -1.594474531383421e-02 + 9.998728744679880e-01j,
+              -1.594474531383421e-02 - 9.998728744679880e-01j]
+        p2 = [-1.766850742887729e-01 + 9.466951258673900e-01j,
+              -1.766850742887729e-01 - 9.466951258673900e-01j,
+               1.467897662432886e-01 + 9.515917126462422e-01j,
+               1.467897662432886e-01 - 9.515917126462422e-01j,
+              -1.370083529426906e-01 + 8.880376681273993e-01j,
+              -1.370083529426906e-01 - 8.880376681273993e-01j,
+               1.086774544701390e-01 + 8.915240810704319e-01j,
+               1.086774544701390e-01 - 8.915240810704319e-01j,
+              -7.982704457700891e-02 + 8.506056315273435e-01j,
+              -7.982704457700891e-02 - 8.506056315273435e-01j,
+               5.238812787110331e-02 + 8.524011102699969e-01j,
+               5.238812787110331e-02 - 8.524011102699969e-01j,
+              -1.357545000491310e-02 + 8.382287744986582e-01j,
+              -1.357545000491310e-02 - 8.382287744986582e-01j]
+        k2 = 4.577122512960063e-01
+        assert_allclose(sorted(z, key=np.imag), sorted(z2, key=np.imag))
+        assert_allclose(sorted(p, key=np.imag), sorted(p2, key=np.imag))
+        assert_allclose(k, k2, rtol=1e-14)
+
+    def test_ba_output(self):
+        b, a = butter(4, [100, 300], 'bandpass', analog=True)
+        b2 = [1.6e+09, 0, 0, 0, 0]
+        a2 = [1.000000000000000e+00, 5.226251859505511e+02,
+              2.565685424949238e+05, 6.794127417357160e+07,
+              1.519411254969542e+10, 2.038238225207147e+12,
+              2.309116882454312e+14, 1.411088002066486e+16,
+              8.099999999999991e+17]
+        assert_allclose(b, b2, rtol=1e-14)
+        assert_allclose(a, a2, rtol=1e-14)
+
+    def test_fs_param(self):
+        for fs in (900, 900.1, 1234.567):
+            for N in (0, 1, 2, 3, 10):
+                for fc in (100, 100.1, 432.12345):
+                    for btype in ('lp', 'hp'):
+                        ba1 = butter(N, fc, btype, fs=fs)
+                        ba2 = butter(N, fc/(fs/2), btype)
+                        assert_allclose(ba1, ba2)
+                for fc in ((100, 200), (100.1, 200.2), (321.123, 432.123)):
+                    for btype in ('bp', 'bs'):
+                        ba1 = butter(N, fc, btype, fs=fs)
+                        for seq in (list, tuple, array):
+                            fcnorm = seq([f/(fs/2) for f in fc])
+                            ba2 = butter(N, fcnorm, btype)
+                            assert_allclose(ba1, ba2)
+
+
+class TestCheby1:
+
+    def test_degenerate(self):
+        # 0-order filter is just a passthrough
+        # Even-order filters have DC gain of -rp dB
+        b, a = cheby1(0, 10*np.log10(2), 1, analog=True)
+        assert_array_almost_equal(b, [1/np.sqrt(2)])
+        assert_array_equal(a, [1])
+
+        # 1-order filter is same for all types
+        b, a = cheby1(1, 10*np.log10(2), 1, analog=True)
+        assert_array_almost_equal(b, [1])
+        assert_array_almost_equal(a, [1, 1])
+
+        z, p, k = cheby1(1, 0.1, 0.3, output='zpk')
+        assert_array_equal(z, [-1])
+        assert_allclose(p, [-5.390126972799615e-01], rtol=1e-14)
+        assert_allclose(k, 7.695063486399808e-01, rtol=1e-14)
+
+    def test_basic(self):
+        for N in range(25):
+            wn = 0.01
+            z, p, k = cheby1(N, 1, wn, 'low', analog=True, output='zpk')
+            assert_array_almost_equal([], z)
+            assert_(len(p) == N)
+            assert_(all(np.real(p) <= 0))  # No poles in right half of S-plane
+
+        for N in range(25):
+            wn = 0.01
+            z, p, k = cheby1(N, 1, wn, 'high', analog=False, output='zpk')
+            assert_array_equal(np.ones(N), z)  # All zeros exactly at DC
+            assert_(all(np.abs(p) <= 1))  # No poles outside unit circle
+
+        # Same test as TestNormalize
+        b, a = cheby1(8, 0.5, 0.048)
+        assert_array_almost_equal(b, [
+                             2.150733144728282e-11, 1.720586515782626e-10,
+                             6.022052805239190e-10, 1.204410561047838e-09,
+                             1.505513201309798e-09, 1.204410561047838e-09,
+                             6.022052805239190e-10, 1.720586515782626e-10,
+                             2.150733144728282e-11], decimal=14)
+        assert_array_almost_equal(a, [
+                             1.000000000000000e+00, -7.782402035027959e+00,
+                             2.654354569747454e+01, -5.182182531666387e+01,
+                             6.334127355102684e+01, -4.963358186631157e+01,
+                             2.434862182949389e+01, -6.836925348604676e+00,
+                             8.412934944449140e-01], decimal=14)
+
+        b, a = cheby1(4, 1, [0.4, 0.7], btype='band')
+        assert_array_almost_equal(b, [0.0084, 0, -0.0335, 0, 0.0502, 0,
+                                      -0.0335, 0, 0.0084], decimal=4)
+        assert_array_almost_equal(a, [1.0, 1.1191, 2.862, 2.2986, 3.4137,
+                                      1.8653, 1.8982, 0.5676, 0.4103],
+                                  decimal=4)
+
+        b2, a2 = cheby1(5, 3, 1, analog=True)
+        assert_array_almost_equal(b2, [0.0626], decimal=4)
+        assert_array_almost_equal(a2, [1, 0.5745, 1.4150, 0.5489, 0.4080,
+                                       0.0626], decimal=4)
+
+        b, a = cheby1(8, 0.5, 0.1)
+        assert_array_almost_equal(b, 1.0e-006 * np.array([
+            0.00703924326028, 0.05631394608227, 0.19709881128793,
+            0.39419762257586, 0.49274702821983, 0.39419762257586,
+            0.19709881128793, 0.05631394608227, 0.00703924326028]),
+            decimal=13)
+        assert_array_almost_equal(a, [
+              1.00000000000000, -7.44912258934158, 24.46749067762108,
+              -46.27560200466141, 55.11160187999928, -42.31640010161038,
+              20.45543300484147, -5.69110270561444, 0.69770374759022],
+            decimal=13)
+
+        b, a = cheby1(8, 0.5, 0.25)
+        assert_array_almost_equal(b, 1.0e-003 * np.array([
+            0.00895261138923, 0.07162089111382, 0.25067311889837,
+            0.50134623779673, 0.62668279724591, 0.50134623779673,
+            0.25067311889837, 0.07162089111382, 0.00895261138923]),
+            decimal=13)
+        assert_array_almost_equal(a, [1.00000000000000, -5.97529229188545,
+                                      16.58122329202101, -27.71423273542923,
+                                      30.39509758355313, -22.34729670426879,
+                                      10.74509800434910, -3.08924633697497,
+                                      0.40707685889802], decimal=13)
+
+    def test_highpass(self):
+        # high even order
+        z, p, k = cheby1(24, 0.7, 0.2, 'high', output='zpk')
+        z2 = np.ones(24)
+        p2 = [-6.136558509657073e-01 + 2.700091504942893e-01j,
+              -6.136558509657073e-01 - 2.700091504942893e-01j,
+              -3.303348340927516e-01 + 6.659400861114254e-01j,
+              -3.303348340927516e-01 - 6.659400861114254e-01j,
+              8.779713780557169e-03 + 8.223108447483040e-01j,
+              8.779713780557169e-03 - 8.223108447483040e-01j,
+              2.742361123006911e-01 + 8.356666951611864e-01j,
+              2.742361123006911e-01 - 8.356666951611864e-01j,
+              4.562984557158206e-01 + 7.954276912303594e-01j,
+              4.562984557158206e-01 - 7.954276912303594e-01j,
+              5.777335494123628e-01 + 7.435821817961783e-01j,
+              5.777335494123628e-01 - 7.435821817961783e-01j,
+              6.593260977749194e-01 + 6.955390907990932e-01j,
+              6.593260977749194e-01 - 6.955390907990932e-01j,
+              7.149590948466562e-01 + 6.559437858502012e-01j,
+              7.149590948466562e-01 - 6.559437858502012e-01j,
+              7.532432388188739e-01 + 6.256158042292060e-01j,
+              7.532432388188739e-01 - 6.256158042292060e-01j,
+              7.794365244268271e-01 + 6.042099234813333e-01j,
+              7.794365244268271e-01 - 6.042099234813333e-01j,
+              7.967253874772997e-01 + 5.911966597313203e-01j,
+              7.967253874772997e-01 - 5.911966597313203e-01j,
+              8.069756417293870e-01 + 5.862214589217275e-01j,
+              8.069756417293870e-01 - 5.862214589217275e-01j]
+        k2 = 6.190427617192018e-04
+        assert_array_equal(z, z2)
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(p2, key=np.imag), rtol=1e-10)
+        assert_allclose(k, k2, rtol=1e-10)
+
+        # high odd order
+        z, p, k = cheby1(23, 0.8, 0.3, 'high', output='zpk')
+        z2 = np.ones(23)
+        p2 = [-7.676400532011010e-01,
+              -6.754621070166477e-01 + 3.970502605619561e-01j,
+              -6.754621070166477e-01 - 3.970502605619561e-01j,
+              -4.528880018446727e-01 + 6.844061483786332e-01j,
+              -4.528880018446727e-01 - 6.844061483786332e-01j,
+              -1.986009130216447e-01 + 8.382285942941594e-01j,
+              -1.986009130216447e-01 - 8.382285942941594e-01j,
+              2.504673931532608e-02 + 8.958137635794080e-01j,
+              2.504673931532608e-02 - 8.958137635794080e-01j,
+              2.001089429976469e-01 + 9.010678290791480e-01j,
+              2.001089429976469e-01 - 9.010678290791480e-01j,
+              3.302410157191755e-01 + 8.835444665962544e-01j,
+              3.302410157191755e-01 - 8.835444665962544e-01j,
+              4.246662537333661e-01 + 8.594054226449009e-01j,
+              4.246662537333661e-01 - 8.594054226449009e-01j,
+              4.919620928120296e-01 + 8.366772762965786e-01j,
+              4.919620928120296e-01 - 8.366772762965786e-01j,
+              5.385746917494749e-01 + 8.191616180796720e-01j,
+              5.385746917494749e-01 - 8.191616180796720e-01j,
+              5.855636993537203e-01 + 8.060680937701062e-01j,
+              5.855636993537203e-01 - 8.060680937701062e-01j,
+              5.688812849391721e-01 + 8.086497795114683e-01j,
+              5.688812849391721e-01 - 8.086497795114683e-01j]
+        k2 = 1.941697029206324e-05
+        assert_array_equal(z, z2)
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(p2, key=np.imag), rtol=1e-10)
+        assert_allclose(k, k2, rtol=1e-10)
+
+        z, p, k = cheby1(10, 1, 1000, 'high', analog=True, output='zpk')
+        z2 = np.zeros(10)
+        p2 = [-3.144743169501551e+03 + 3.511680029092744e+03j,
+              -3.144743169501551e+03 - 3.511680029092744e+03j,
+              -5.633065604514602e+02 + 2.023615191183945e+03j,
+              -5.633065604514602e+02 - 2.023615191183945e+03j,
+              -1.946412183352025e+02 + 1.372309454274755e+03j,
+              -1.946412183352025e+02 - 1.372309454274755e+03j,
+              -7.987162953085479e+01 + 1.105207708045358e+03j,
+              -7.987162953085479e+01 - 1.105207708045358e+03j,
+              -2.250315039031946e+01 + 1.001723931471477e+03j,
+              -2.250315039031946e+01 - 1.001723931471477e+03j]
+        k2 = 8.912509381337453e-01
+        assert_array_equal(z, z2)
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(p2, key=np.imag), rtol=1e-13)
+        assert_allclose(k, k2, rtol=1e-15)
+
+    def test_bandpass(self):
+        z, p, k = cheby1(8, 1, [0.3, 0.4], 'bp', output='zpk')
+        z2 = [1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1]
+        p2 = [3.077784854851463e-01 + 9.453307017592942e-01j,
+              3.077784854851463e-01 - 9.453307017592942e-01j,
+              3.280567400654425e-01 + 9.272377218689016e-01j,
+              3.280567400654425e-01 - 9.272377218689016e-01j,
+              3.677912763284301e-01 + 9.038008865279966e-01j,
+              3.677912763284301e-01 - 9.038008865279966e-01j,
+              4.194425632520948e-01 + 8.769407159656157e-01j,
+              4.194425632520948e-01 - 8.769407159656157e-01j,
+              4.740921994669189e-01 + 8.496508528630974e-01j,
+              4.740921994669189e-01 - 8.496508528630974e-01j,
+              5.234866481897429e-01 + 8.259608422808477e-01j,
+              5.234866481897429e-01 - 8.259608422808477e-01j,
+              5.844717632289875e-01 + 8.052901363500210e-01j,
+              5.844717632289875e-01 - 8.052901363500210e-01j,
+              5.615189063336070e-01 + 8.100667803850766e-01j,
+              5.615189063336070e-01 - 8.100667803850766e-01j]
+        k2 = 5.007028718074307e-09
+        assert_array_equal(z, z2)
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(p2, key=np.imag), rtol=1e-13)
+        assert_allclose(k, k2, rtol=1e-13)
+
+    def test_bandstop(self):
+        z, p, k = cheby1(7, 1, [0.5, 0.6], 'stop', output='zpk')
+        z2 = [-1.583844403245361e-01 + 9.873775210440450e-01j,
+              -1.583844403245361e-01 - 9.873775210440450e-01j,
+              -1.583844403245361e-01 + 9.873775210440450e-01j,
+              -1.583844403245361e-01 - 9.873775210440450e-01j,
+              -1.583844403245361e-01 + 9.873775210440450e-01j,
+              -1.583844403245361e-01 - 9.873775210440450e-01j,
+              -1.583844403245361e-01 + 9.873775210440450e-01j,
+              -1.583844403245361e-01 - 9.873775210440450e-01j,
+              -1.583844403245361e-01 + 9.873775210440450e-01j,
+              -1.583844403245361e-01 - 9.873775210440450e-01j,
+              -1.583844403245361e-01 + 9.873775210440450e-01j,
+              -1.583844403245361e-01 - 9.873775210440450e-01j,
+              -1.583844403245361e-01 + 9.873775210440450e-01j,
+              -1.583844403245361e-01 - 9.873775210440450e-01j]
+        p2 = [-8.942974551472813e-02 + 3.482480481185926e-01j,
+              -8.942974551472813e-02 - 3.482480481185926e-01j,
+               1.293775154041798e-01 + 8.753499858081858e-01j,
+               1.293775154041798e-01 - 8.753499858081858e-01j,
+               3.399741945062013e-02 + 9.690316022705607e-01j,
+               3.399741945062013e-02 - 9.690316022705607e-01j,
+               4.167225522796539e-04 + 9.927338161087488e-01j,
+               4.167225522796539e-04 - 9.927338161087488e-01j,
+              -3.912966549550960e-01 + 8.046122859255742e-01j,
+              -3.912966549550960e-01 - 8.046122859255742e-01j,
+              -3.307805547127368e-01 + 9.133455018206508e-01j,
+              -3.307805547127368e-01 - 9.133455018206508e-01j,
+              -3.072658345097743e-01 + 9.443589759799366e-01j,
+              -3.072658345097743e-01 - 9.443589759799366e-01j]
+        k2 = 3.619438310405028e-01
+        assert_allclose(sorted(z, key=np.imag),
+                        sorted(z2, key=np.imag), rtol=1e-13)
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(p2, key=np.imag), rtol=1e-13)
+        assert_allclose(k, k2, rtol=0, atol=5e-16)
+
+    def test_ba_output(self):
+        # with transfer function conversion,  without digital conversion
+        b, a = cheby1(5, 0.9, [210, 310], 'stop', analog=True)
+        b2 = [1.000000000000006e+00, 0,
+              3.255000000000020e+05, 0,
+              4.238010000000026e+10, 0,
+              2.758944510000017e+15, 0,
+              8.980364380050052e+19, 0,
+              1.169243442282517e+24
+              ]
+        a2 = [1.000000000000000e+00, 4.630555945694342e+02,
+              4.039266454794788e+05, 1.338060988610237e+08,
+              5.844333551294591e+10, 1.357346371637638e+13,
+              3.804661141892782e+15, 5.670715850340080e+17,
+              1.114411200988328e+20, 8.316815934908471e+21,
+              1.169243442282517e+24
+              ]
+        assert_allclose(b, b2, rtol=1e-14)
+        assert_allclose(a, a2, rtol=1e-14)
+
+    def test_fs_param(self):
+        for fs in (900, 900.1, 1234.567):
+            for N in (0, 1, 2, 3, 10):
+                for fc in (100, 100.1, 432.12345):
+                    for btype in ('lp', 'hp'):
+                        ba1 = cheby1(N, 1, fc, btype, fs=fs)
+                        ba2 = cheby1(N, 1, fc/(fs/2), btype)
+                        assert_allclose(ba1, ba2)
+                for fc in ((100, 200), (100.1, 200.2), (321.123, 432.123)):
+                    for btype in ('bp', 'bs'):
+                        ba1 = cheby1(N, 1, fc, btype, fs=fs)
+                        for seq in (list, tuple, array):
+                            fcnorm = seq([f/(fs/2) for f in fc])
+                            ba2 = cheby1(N, 1, fcnorm, btype)
+                            assert_allclose(ba1, ba2)
+
+
+class TestCheby2:
+
+    def test_degenerate(self):
+        # 0-order filter is just a passthrough
+        # Stopband ripple factor doesn't matter
+        b, a = cheby2(0, 123.456, 1, analog=True)
+        assert_array_equal(b, [1])
+        assert_array_equal(a, [1])
+
+        # 1-order filter is same for all types
+        b, a = cheby2(1, 10*np.log10(2), 1, analog=True)
+        assert_array_almost_equal(b, [1])
+        assert_array_almost_equal(a, [1, 1])
+
+        z, p, k = cheby2(1, 50, 0.3, output='zpk')
+        assert_array_equal(z, [-1])
+        assert_allclose(p, [9.967826460175649e-01], rtol=1e-14)
+        assert_allclose(k, 1.608676991217512e-03, rtol=1e-14)
+
+    def test_basic(self):
+        for N in range(25):
+            wn = 0.01
+            z, p, k = cheby2(N, 40, wn, 'low', analog=True, output='zpk')
+            assert_(len(p) == N)
+            assert_(all(np.real(p) <= 0))  # No poles in right half of S-plane
+
+        for N in range(25):
+            wn = 0.01
+            z, p, k = cheby2(N, 40, wn, 'high', analog=False, output='zpk')
+            assert_(all(np.abs(p) <= 1))  # No poles outside unit circle
+
+        B, A = cheby2(18, 100, 0.5)
+        assert_array_almost_equal(B, [
+            0.00167583914216, 0.01249479541868, 0.05282702120282,
+            0.15939804265706, 0.37690207631117, 0.73227013789108,
+            1.20191856962356, 1.69522872823393, 2.07598674519837,
+            2.21972389625291, 2.07598674519838, 1.69522872823395,
+            1.20191856962359, 0.73227013789110, 0.37690207631118,
+            0.15939804265707, 0.05282702120282, 0.01249479541868,
+            0.00167583914216], decimal=13)
+        assert_array_almost_equal(A, [
+            1.00000000000000, -0.27631970006174, 3.19751214254060,
+            -0.15685969461355, 4.13926117356269, 0.60689917820044,
+            2.95082770636540, 0.89016501910416, 1.32135245849798,
+            0.51502467236824, 0.38906643866660, 0.15367372690642,
+            0.07255803834919, 0.02422454070134, 0.00756108751837,
+            0.00179848550988, 0.00033713574499, 0.00004258794833,
+            0.00000281030149], decimal=13)
+
+    def test_highpass(self):
+        # high even order
+        z, p, k = cheby2(26, 60, 0.3, 'high', output='zpk')
+        z2 = [9.981088955489852e-01 + 6.147058341984388e-02j,
+              9.981088955489852e-01 - 6.147058341984388e-02j,
+              9.832702870387426e-01 + 1.821525257215483e-01j,
+              9.832702870387426e-01 - 1.821525257215483e-01j,
+              9.550760158089112e-01 + 2.963609353922882e-01j,
+              9.550760158089112e-01 - 2.963609353922882e-01j,
+              9.162054748821922e-01 + 4.007087817803773e-01j,
+              9.162054748821922e-01 - 4.007087817803773e-01j,
+              8.700619897368064e-01 + 4.929423232136168e-01j,
+              8.700619897368064e-01 - 4.929423232136168e-01j,
+              5.889791753434985e-01 + 8.081482110427953e-01j,
+              5.889791753434985e-01 - 8.081482110427953e-01j,
+              5.984900456570295e-01 + 8.011302423760501e-01j,
+              5.984900456570295e-01 - 8.011302423760501e-01j,
+              6.172880888914629e-01 + 7.867371958365343e-01j,
+              6.172880888914629e-01 - 7.867371958365343e-01j,
+              6.448899971038180e-01 + 7.642754030030161e-01j,
+              6.448899971038180e-01 - 7.642754030030161e-01j,
+              6.804845629637927e-01 + 7.327624168637228e-01j,
+              6.804845629637927e-01 - 7.327624168637228e-01j,
+              8.202619107108660e-01 + 5.719881098737678e-01j,
+              8.202619107108660e-01 - 5.719881098737678e-01j,
+              7.228410452536148e-01 + 6.910143437705678e-01j,
+              7.228410452536148e-01 - 6.910143437705678e-01j,
+              7.702121399578629e-01 + 6.377877856007792e-01j,
+              7.702121399578629e-01 - 6.377877856007792e-01j]
+        p2 = [7.365546198286450e-01 + 4.842085129329526e-02j,
+              7.365546198286450e-01 - 4.842085129329526e-02j,
+              7.292038510962885e-01 + 1.442201672097581e-01j,
+              7.292038510962885e-01 - 1.442201672097581e-01j,
+              7.151293788040354e-01 + 2.369925800458584e-01j,
+              7.151293788040354e-01 - 2.369925800458584e-01j,
+              6.955051820787286e-01 + 3.250341363856910e-01j,
+              6.955051820787286e-01 - 3.250341363856910e-01j,
+              6.719122956045220e-01 + 4.070475750638047e-01j,
+              6.719122956045220e-01 - 4.070475750638047e-01j,
+              6.461722130611300e-01 + 4.821965916689270e-01j,
+              6.461722130611300e-01 - 4.821965916689270e-01j,
+              5.528045062872224e-01 + 8.162920513838372e-01j,
+              5.528045062872224e-01 - 8.162920513838372e-01j,
+              5.464847782492791e-01 + 7.869899955967304e-01j,
+              5.464847782492791e-01 - 7.869899955967304e-01j,
+              5.488033111260949e-01 + 7.520442354055579e-01j,
+              5.488033111260949e-01 - 7.520442354055579e-01j,
+              6.201874719022955e-01 + 5.500894392527353e-01j,
+              6.201874719022955e-01 - 5.500894392527353e-01j,
+              5.586478152536709e-01 + 7.112676877332921e-01j,
+              5.586478152536709e-01 - 7.112676877332921e-01j,
+              5.958145844148228e-01 + 6.107074340842115e-01j,
+              5.958145844148228e-01 - 6.107074340842115e-01j,
+              5.747812938519067e-01 + 6.643001536914696e-01j,
+              5.747812938519067e-01 - 6.643001536914696e-01j]
+        k2 = 9.932997786497189e-02
+        assert_allclose(sorted(z, key=np.angle),
+                        sorted(z2, key=np.angle), rtol=1e-13)
+        assert_allclose(sorted(p, key=np.angle),
+                        sorted(p2, key=np.angle), rtol=1e-12)
+        assert_allclose(k, k2, rtol=1e-11)
+
+        # high odd order
+        z, p, k = cheby2(25, 80, 0.5, 'high', output='zpk')
+        z2 = [9.690690376586687e-01 + 2.467897896011971e-01j,
+              9.690690376586687e-01 - 2.467897896011971e-01j,
+              9.999999999999492e-01,
+              8.835111277191199e-01 + 4.684101698261429e-01j,
+              8.835111277191199e-01 - 4.684101698261429e-01j,
+              7.613142857900539e-01 + 6.483830335935022e-01j,
+              7.613142857900539e-01 - 6.483830335935022e-01j,
+              6.232625173626231e-01 + 7.820126817709752e-01j,
+              6.232625173626231e-01 - 7.820126817709752e-01j,
+              4.864456563413621e-01 + 8.737108351316745e-01j,
+              4.864456563413621e-01 - 8.737108351316745e-01j,
+              3.618368136816749e-01 + 9.322414495530347e-01j,
+              3.618368136816749e-01 - 9.322414495530347e-01j,
+              2.549486883466794e-01 + 9.669545833752675e-01j,
+              2.549486883466794e-01 - 9.669545833752675e-01j,
+              1.676175432109457e-01 + 9.858520980390212e-01j,
+              1.676175432109457e-01 - 9.858520980390212e-01j,
+              1.975218468277521e-03 + 9.999980492540941e-01j,
+              1.975218468277521e-03 - 9.999980492540941e-01j,
+              1.786959496651858e-02 + 9.998403260399917e-01j,
+              1.786959496651858e-02 - 9.998403260399917e-01j,
+              9.967933660557139e-02 + 9.950196127985684e-01j,
+              9.967933660557139e-02 - 9.950196127985684e-01j,
+              5.013970951219547e-02 + 9.987422137518890e-01j,
+              5.013970951219547e-02 - 9.987422137518890e-01j]
+        p2 = [4.218866331906864e-01,
+              4.120110200127552e-01 + 1.361290593621978e-01j,
+              4.120110200127552e-01 - 1.361290593621978e-01j,
+              3.835890113632530e-01 + 2.664910809911026e-01j,
+              3.835890113632530e-01 - 2.664910809911026e-01j,
+              3.399195570456499e-01 + 3.863983538639875e-01j,
+              3.399195570456499e-01 - 3.863983538639875e-01j,
+              2.855977834508353e-01 + 4.929444399540688e-01j,
+              2.855977834508353e-01 - 4.929444399540688e-01j,
+              2.255765441339322e-01 + 5.851631870205766e-01j,
+              2.255765441339322e-01 - 5.851631870205766e-01j,
+              1.644087535815792e-01 + 6.637356937277153e-01j,
+              1.644087535815792e-01 - 6.637356937277153e-01j,
+              -7.293633845273095e-02 + 9.739218252516307e-01j,
+              -7.293633845273095e-02 - 9.739218252516307e-01j,
+              1.058259206358626e-01 + 7.304739464862978e-01j,
+              1.058259206358626e-01 - 7.304739464862978e-01j,
+              -5.703971947785402e-02 + 9.291057542169088e-01j,
+              -5.703971947785402e-02 - 9.291057542169088e-01j,
+              5.263875132656864e-02 + 7.877974334424453e-01j,
+              5.263875132656864e-02 - 7.877974334424453e-01j,
+              -3.007943405982616e-02 + 8.846331716180016e-01j,
+              -3.007943405982616e-02 - 8.846331716180016e-01j,
+              6.857277464483946e-03 + 8.383275456264492e-01j,
+              6.857277464483946e-03 - 8.383275456264492e-01j]
+        k2 = 6.507068761705037e-03
+        assert_allclose(sorted(z, key=np.angle),
+                        sorted(z2, key=np.angle), rtol=1e-13)
+        assert_allclose(sorted(p, key=np.angle),
+                        sorted(p2, key=np.angle), rtol=1e-12)
+        assert_allclose(k, k2, rtol=1e-11)
+
+    def test_bandpass(self):
+        z, p, k = cheby2(9, 40, [0.07, 0.2], 'pass', output='zpk')
+        z2 = [-9.999999999999999e-01,
+               3.676588029658514e-01 + 9.299607543341383e-01j,
+               3.676588029658514e-01 - 9.299607543341383e-01j,
+               7.009689684982283e-01 + 7.131917730894889e-01j,
+               7.009689684982283e-01 - 7.131917730894889e-01j,
+               7.815697973765858e-01 + 6.238178033919218e-01j,
+               7.815697973765858e-01 - 6.238178033919218e-01j,
+               8.063793628819866e-01 + 5.913986160941200e-01j,
+               8.063793628819866e-01 - 5.913986160941200e-01j,
+               1.000000000000001e+00,
+               9.944493019920448e-01 + 1.052168511576739e-01j,
+               9.944493019920448e-01 - 1.052168511576739e-01j,
+               9.854674703367308e-01 + 1.698642543566085e-01j,
+               9.854674703367308e-01 - 1.698642543566085e-01j,
+               9.762751735919308e-01 + 2.165335665157851e-01j,
+               9.762751735919308e-01 - 2.165335665157851e-01j,
+               9.792277171575134e-01 + 2.027636011479496e-01j,
+               9.792277171575134e-01 - 2.027636011479496e-01j]
+        p2 = [8.143803410489621e-01 + 5.411056063397541e-01j,
+              8.143803410489621e-01 - 5.411056063397541e-01j,
+              7.650769827887418e-01 + 5.195412242095543e-01j,
+              7.650769827887418e-01 - 5.195412242095543e-01j,
+              6.096241204063443e-01 + 3.568440484659796e-01j,
+              6.096241204063443e-01 - 3.568440484659796e-01j,
+              6.918192770246239e-01 + 4.770463577106911e-01j,
+              6.918192770246239e-01 - 4.770463577106911e-01j,
+              6.986241085779207e-01 + 1.146512226180060e-01j,
+              6.986241085779207e-01 - 1.146512226180060e-01j,
+              8.654645923909734e-01 + 1.604208797063147e-01j,
+              8.654645923909734e-01 - 1.604208797063147e-01j,
+              9.164831670444591e-01 + 1.969181049384918e-01j,
+              9.164831670444591e-01 - 1.969181049384918e-01j,
+              9.630425777594550e-01 + 2.317513360702271e-01j,
+              9.630425777594550e-01 - 2.317513360702271e-01j,
+              9.438104703725529e-01 + 2.193509900269860e-01j,
+              9.438104703725529e-01 - 2.193509900269860e-01j]
+        k2 = 9.345352824659604e-03
+        assert_allclose(sorted(z, key=np.angle),
+                        sorted(z2, key=np.angle), rtol=1e-13)
+        assert_allclose(sorted(p, key=np.angle),
+                        sorted(p2, key=np.angle), rtol=1e-13)
+        assert_allclose(k, k2, rtol=1e-11)
+
+    def test_bandstop(self):
+        z, p, k = cheby2(6, 55, [0.1, 0.9], 'stop', output='zpk')
+        z2 = [6.230544895101009e-01 + 7.821784343111114e-01j,
+              6.230544895101009e-01 - 7.821784343111114e-01j,
+              9.086608545660115e-01 + 4.175349702471991e-01j,
+              9.086608545660115e-01 - 4.175349702471991e-01j,
+              9.478129721465802e-01 + 3.188268649763867e-01j,
+              9.478129721465802e-01 - 3.188268649763867e-01j,
+              -6.230544895100982e-01 + 7.821784343111109e-01j,
+              -6.230544895100982e-01 - 7.821784343111109e-01j,
+              -9.086608545660116e-01 + 4.175349702472088e-01j,
+              -9.086608545660116e-01 - 4.175349702472088e-01j,
+              -9.478129721465784e-01 + 3.188268649763897e-01j,
+              -9.478129721465784e-01 - 3.188268649763897e-01j]
+        p2 = [-9.464094036167638e-01 + 1.720048695084344e-01j,
+              -9.464094036167638e-01 - 1.720048695084344e-01j,
+              -8.715844103386737e-01 + 1.370665039509297e-01j,
+              -8.715844103386737e-01 - 1.370665039509297e-01j,
+              -8.078751204586425e-01 + 5.729329866682983e-02j,
+              -8.078751204586425e-01 - 5.729329866682983e-02j,
+               9.464094036167665e-01 + 1.720048695084332e-01j,
+               9.464094036167665e-01 - 1.720048695084332e-01j,
+               8.078751204586447e-01 + 5.729329866683007e-02j,
+               8.078751204586447e-01 - 5.729329866683007e-02j,
+               8.715844103386721e-01 + 1.370665039509331e-01j,
+               8.715844103386721e-01 - 1.370665039509331e-01j]
+        k2 = 2.917823332763358e-03
+        assert_allclose(sorted(z, key=np.angle),
+                        sorted(z2, key=np.angle), rtol=1e-13)
+        assert_allclose(sorted(p, key=np.angle),
+                        sorted(p2, key=np.angle), rtol=1e-13)
+        assert_allclose(k, k2, rtol=1e-11)
+
+    def test_ba_output(self):
+        # with transfer function conversion, without digital conversion
+        b, a = cheby2(5, 20, [2010, 2100], 'stop', True)
+        b2 = [1.000000000000000e+00, 0,  # Matlab: 6.683253076978249e-12,
+              2.111512500000000e+07, 0,  # Matlab: 1.134325604589552e-04,
+              1.782966433781250e+14, 0,  # Matlab: 7.216787944356781e+02,
+              7.525901316990656e+20, 0,  # Matlab: 2.039829265789886e+09,
+              1.587960565565748e+27, 0,  # Matlab: 2.161236218626134e+15,
+              1.339913493808585e+33]
+        a2 = [1.000000000000000e+00, 1.849550755473371e+02,
+              2.113222918998538e+07, 3.125114149732283e+09,
+              1.785133457155609e+14, 1.979158697776348e+16,
+              7.535048322653831e+20, 5.567966191263037e+22,
+              1.589246884221346e+27, 5.871210648525566e+28,
+              1.339913493808590e+33]
+        assert_allclose(b, b2, rtol=1e-14)
+        assert_allclose(a, a2, rtol=1e-14)
+
+    def test_fs_param(self):
+        for fs in (900, 900.1, 1234.567):
+            for N in (0, 1, 2, 3, 10):
+                for fc in (100, 100.1, 432.12345):
+                    for btype in ('lp', 'hp'):
+                        ba1 = cheby2(N, 20, fc, btype, fs=fs)
+                        ba2 = cheby2(N, 20, fc/(fs/2), btype)
+                        assert_allclose(ba1, ba2)
+                for fc in ((100, 200), (100.1, 200.2), (321.123, 432.123)):
+                    for btype in ('bp', 'bs'):
+                        ba1 = cheby2(N, 20, fc, btype, fs=fs)
+                        for seq in (list, tuple, array):
+                            fcnorm = seq([f/(fs/2) for f in fc])
+                            ba2 = cheby2(N, 20, fcnorm, btype)
+                            assert_allclose(ba1, ba2)
+
+class TestEllip:
+
+    def test_degenerate(self):
+        # 0-order filter is just a passthrough
+        # Even-order filters have DC gain of -rp dB
+        # Stopband ripple factor doesn't matter
+        b, a = ellip(0, 10*np.log10(2), 123.456, 1, analog=True)
+        assert_array_almost_equal(b, [1/np.sqrt(2)])
+        assert_array_equal(a, [1])
+
+        # 1-order filter is same for all types
+        b, a = ellip(1, 10*np.log10(2), 1, 1, analog=True)
+        assert_array_almost_equal(b, [1])
+        assert_array_almost_equal(a, [1, 1])
+
+        z, p, k = ellip(1, 1, 55, 0.3, output='zpk')
+        assert_allclose(z, [-9.999999999999998e-01], rtol=1e-14)
+        assert_allclose(p, [-6.660721153525525e-04], rtol=1e-10)
+        assert_allclose(k, 5.003330360576763e-01, rtol=1e-14)
+
+    def test_basic(self):
+        for N in range(25):
+            wn = 0.01
+            z, p, k = ellip(N, 1, 40, wn, 'low', analog=True, output='zpk')
+            assert_(len(p) == N)
+            assert_(all(np.real(p) <= 0))  # No poles in right half of S-plane
+
+        for N in range(25):
+            wn = 0.01
+            z, p, k = ellip(N, 1, 40, wn, 'high', analog=False, output='zpk')
+            assert_(all(np.abs(p) <= 1))  # No poles outside unit circle
+
+        b3, a3 = ellip(5, 3, 26, 1, analog=True)
+        assert_array_almost_equal(b3, [0.1420, 0, 0.3764, 0,
+                                       0.2409], decimal=4)
+        assert_array_almost_equal(a3, [1, 0.5686, 1.8061, 0.8017, 0.8012,
+                                       0.2409], decimal=4)
+
+        b, a = ellip(3, 1, 60, [0.4, 0.7], 'stop')
+        assert_array_almost_equal(b, [0.3310, 0.3469, 1.1042, 0.7044, 1.1042,
+                                      0.3469, 0.3310], decimal=4)
+        assert_array_almost_equal(a, [1.0000, 0.6973, 1.1441, 0.5878, 0.7323,
+                                      0.1131, -0.0060], decimal=4)
+
+    def test_highpass(self):
+        # high even order
+        z, p, k = ellip(24, 1, 80, 0.3, 'high', output='zpk')
+        z2 = [9.761875332501075e-01 + 2.169283290099910e-01j,
+              9.761875332501075e-01 - 2.169283290099910e-01j,
+              8.413503353963494e-01 + 5.404901600661900e-01j,
+              8.413503353963494e-01 - 5.404901600661900e-01j,
+              7.160082576305009e-01 + 6.980918098681732e-01j,
+              7.160082576305009e-01 - 6.980918098681732e-01j,
+              6.456533638965329e-01 + 7.636306264739803e-01j,
+              6.456533638965329e-01 - 7.636306264739803e-01j,
+              6.127321820971366e-01 + 7.902906256703928e-01j,
+              6.127321820971366e-01 - 7.902906256703928e-01j,
+              5.983607817490196e-01 + 8.012267936512676e-01j,
+              5.983607817490196e-01 - 8.012267936512676e-01j,
+              5.922577552594799e-01 + 8.057485658286990e-01j,
+              5.922577552594799e-01 - 8.057485658286990e-01j,
+              5.896952092563588e-01 + 8.076258788449631e-01j,
+              5.896952092563588e-01 - 8.076258788449631e-01j,
+              5.886248765538837e-01 + 8.084063054565607e-01j,
+              5.886248765538837e-01 - 8.084063054565607e-01j,
+              5.881802711123132e-01 + 8.087298490066037e-01j,
+              5.881802711123132e-01 - 8.087298490066037e-01j,
+              5.879995719101164e-01 + 8.088612386766461e-01j,
+              5.879995719101164e-01 - 8.088612386766461e-01j,
+              5.879354086709576e-01 + 8.089078780868164e-01j,
+              5.879354086709576e-01 - 8.089078780868164e-01j]
+        p2 = [-3.184805259081650e-01 + 4.206951906775851e-01j,
+              -3.184805259081650e-01 - 4.206951906775851e-01j,
+               1.417279173459985e-01 + 7.903955262836452e-01j,
+               1.417279173459985e-01 - 7.903955262836452e-01j,
+               4.042881216964651e-01 + 8.309042239116594e-01j,
+               4.042881216964651e-01 - 8.309042239116594e-01j,
+               5.128964442789670e-01 + 8.229563236799665e-01j,
+               5.128964442789670e-01 - 8.229563236799665e-01j,
+               5.569614712822724e-01 + 8.155957702908510e-01j,
+               5.569614712822724e-01 - 8.155957702908510e-01j,
+               5.750478870161392e-01 + 8.118633973883931e-01j,
+               5.750478870161392e-01 - 8.118633973883931e-01j,
+               5.825314018170804e-01 + 8.101960910679270e-01j,
+               5.825314018170804e-01 - 8.101960910679270e-01j,
+               5.856397379751872e-01 + 8.094825218722543e-01j,
+               5.856397379751872e-01 - 8.094825218722543e-01j,
+               5.869326035251949e-01 + 8.091827531557583e-01j,
+               5.869326035251949e-01 - 8.091827531557583e-01j,
+               5.874697218855733e-01 + 8.090593298213502e-01j,
+               5.874697218855733e-01 - 8.090593298213502e-01j,
+               5.876904783532237e-01 + 8.090127161018823e-01j,
+               5.876904783532237e-01 - 8.090127161018823e-01j,
+               5.877753105317594e-01 + 8.090050577978136e-01j,
+               5.877753105317594e-01 - 8.090050577978136e-01j]
+        k2 = 4.918081266957108e-02
+        assert_allclose(sorted(z, key=np.angle),
+                        sorted(z2, key=np.angle), rtol=1e-4)
+        assert_allclose(sorted(p, key=np.angle),
+                        sorted(p2, key=np.angle), rtol=1e-4)
+        assert_allclose(k, k2, rtol=1e-3)
+
+        # high odd order
+        z, p, k = ellip(23, 1, 70, 0.5, 'high', output='zpk')
+        z2 = [9.999999999998661e-01,
+              6.603717261750994e-01 + 7.509388678638675e-01j,
+              6.603717261750994e-01 - 7.509388678638675e-01j,
+              2.788635267510325e-01 + 9.603307416968041e-01j,
+              2.788635267510325e-01 - 9.603307416968041e-01j,
+              1.070215532544218e-01 + 9.942567008268131e-01j,
+              1.070215532544218e-01 - 9.942567008268131e-01j,
+              4.049427369978163e-02 + 9.991797705105507e-01j,
+              4.049427369978163e-02 - 9.991797705105507e-01j,
+              1.531059368627931e-02 + 9.998827859909265e-01j,
+              1.531059368627931e-02 - 9.998827859909265e-01j,
+              5.808061438534933e-03 + 9.999831330689181e-01j,
+              5.808061438534933e-03 - 9.999831330689181e-01j,
+              2.224277847754599e-03 + 9.999975262909676e-01j,
+              2.224277847754599e-03 - 9.999975262909676e-01j,
+              8.731857107534554e-04 + 9.999996187732845e-01j,
+              8.731857107534554e-04 - 9.999996187732845e-01j,
+              3.649057346914968e-04 + 9.999999334218996e-01j,
+              3.649057346914968e-04 - 9.999999334218996e-01j,
+              1.765538109802615e-04 + 9.999999844143768e-01j,
+              1.765538109802615e-04 - 9.999999844143768e-01j,
+              1.143655290967426e-04 + 9.999999934602630e-01j,
+              1.143655290967426e-04 - 9.999999934602630e-01j]
+        p2 = [-6.322017026545028e-01,
+              -4.648423756662754e-01 + 5.852407464440732e-01j,
+              -4.648423756662754e-01 - 5.852407464440732e-01j,
+              -2.249233374627773e-01 + 8.577853017985717e-01j,
+              -2.249233374627773e-01 - 8.577853017985717e-01j,
+              -9.234137570557621e-02 + 9.506548198678851e-01j,
+              -9.234137570557621e-02 - 9.506548198678851e-01j,
+              -3.585663561241373e-02 + 9.821494736043981e-01j,
+              -3.585663561241373e-02 - 9.821494736043981e-01j,
+              -1.363917242312723e-02 + 9.933844128330656e-01j,
+              -1.363917242312723e-02 - 9.933844128330656e-01j,
+              -5.131505238923029e-03 + 9.975221173308673e-01j,
+              -5.131505238923029e-03 - 9.975221173308673e-01j,
+              -1.904937999259502e-03 + 9.990680819857982e-01j,
+              -1.904937999259502e-03 - 9.990680819857982e-01j,
+              -6.859439885466834e-04 + 9.996492201426826e-01j,
+              -6.859439885466834e-04 - 9.996492201426826e-01j,
+              -2.269936267937089e-04 + 9.998686250679161e-01j,
+              -2.269936267937089e-04 - 9.998686250679161e-01j,
+              -5.687071588789117e-05 + 9.999527573294513e-01j,
+              -5.687071588789117e-05 - 9.999527573294513e-01j,
+              -6.948417068525226e-07 + 9.999882737700173e-01j,
+              -6.948417068525226e-07 - 9.999882737700173e-01j]
+        k2 = 1.220910020289434e-02
+        assert_allclose(sorted(z, key=np.angle),
+                        sorted(z2, key=np.angle), rtol=1e-4)
+        assert_allclose(sorted(p, key=np.angle),
+                        sorted(p2, key=np.angle), rtol=1e-4)
+        assert_allclose(k, k2, rtol=1e-3)
+
+    def test_bandpass(self):
+        z, p, k = ellip(7, 1, 40, [0.07, 0.2], 'pass', output='zpk')
+        z2 = [-9.999999999999991e-01,
+               6.856610961780020e-01 + 7.279209168501619e-01j,
+               6.856610961780020e-01 - 7.279209168501619e-01j,
+               7.850346167691289e-01 + 6.194518952058737e-01j,
+               7.850346167691289e-01 - 6.194518952058737e-01j,
+               7.999038743173071e-01 + 6.001281461922627e-01j,
+               7.999038743173071e-01 - 6.001281461922627e-01j,
+               9.999999999999999e-01,
+               9.862938983554124e-01 + 1.649980183725925e-01j,
+               9.862938983554124e-01 - 1.649980183725925e-01j,
+               9.788558330548762e-01 + 2.045513580850601e-01j,
+               9.788558330548762e-01 - 2.045513580850601e-01j,
+               9.771155231720003e-01 + 2.127093189691258e-01j,
+               9.771155231720003e-01 - 2.127093189691258e-01j]
+        p2 = [8.063992755498643e-01 + 5.858071374778874e-01j,
+              8.063992755498643e-01 - 5.858071374778874e-01j,
+              8.050395347071724e-01 + 5.639097428109795e-01j,
+              8.050395347071724e-01 - 5.639097428109795e-01j,
+              8.113124936559144e-01 + 4.855241143973142e-01j,
+              8.113124936559144e-01 - 4.855241143973142e-01j,
+              8.665595314082394e-01 + 3.334049560919331e-01j,
+              8.665595314082394e-01 - 3.334049560919331e-01j,
+              9.412369011968871e-01 + 2.457616651325908e-01j,
+              9.412369011968871e-01 - 2.457616651325908e-01j,
+              9.679465190411238e-01 + 2.228772501848216e-01j,
+              9.679465190411238e-01 - 2.228772501848216e-01j,
+              9.747235066273385e-01 + 2.178937926146544e-01j,
+              9.747235066273385e-01 - 2.178937926146544e-01j]
+        k2 = 8.354782670263239e-03
+        assert_allclose(sorted(z, key=np.angle),
+                        sorted(z2, key=np.angle), rtol=1e-4)
+        assert_allclose(sorted(p, key=np.angle),
+                        sorted(p2, key=np.angle), rtol=1e-4)
+        assert_allclose(k, k2, rtol=1e-3)
+
+        z, p, k = ellip(5, 1, 75, [90.5, 110.5], 'pass', True, 'zpk')
+        z2 = [-5.583607317695175e-14 + 1.433755965989225e+02j,
+              -5.583607317695175e-14 - 1.433755965989225e+02j,
+               5.740106416459296e-14 + 1.261678754570291e+02j,
+               5.740106416459296e-14 - 1.261678754570291e+02j,
+              -2.199676239638652e-14 + 6.974861996895196e+01j,
+              -2.199676239638652e-14 - 6.974861996895196e+01j,
+              -3.372595657044283e-14 + 7.926145989044531e+01j,
+              -3.372595657044283e-14 - 7.926145989044531e+01j,
+              0]
+        p2 = [-8.814960004852743e-01 + 1.104124501436066e+02j,
+              -8.814960004852743e-01 - 1.104124501436066e+02j,
+              -2.477372459140184e+00 + 1.065638954516534e+02j,
+              -2.477372459140184e+00 - 1.065638954516534e+02j,
+              -3.072156842945799e+00 + 9.995404870405324e+01j,
+              -3.072156842945799e+00 - 9.995404870405324e+01j,
+              -2.180456023925693e+00 + 9.379206865455268e+01j,
+              -2.180456023925693e+00 - 9.379206865455268e+01j,
+              -7.230484977485752e-01 + 9.056598800801140e+01j,
+              -7.230484977485752e-01 - 9.056598800801140e+01j]
+        k2 = 3.774571622827070e-02
+        assert_allclose(sorted(z, key=np.imag),
+                        sorted(z2, key=np.imag), rtol=1e-4)
+        assert_allclose(sorted(p, key=np.imag),
+                        sorted(p2, key=np.imag), rtol=1e-6)
+        assert_allclose(k, k2, rtol=1e-3)
+
+    def test_bandstop(self):
+        z, p, k = ellip(8, 1, 65, [0.2, 0.4], 'stop', output='zpk')
+        z2 = [3.528578094286510e-01 + 9.356769561794296e-01j,
+              3.528578094286510e-01 - 9.356769561794296e-01j,
+              3.769716042264783e-01 + 9.262248159096587e-01j,
+              3.769716042264783e-01 - 9.262248159096587e-01j,
+              4.406101783111199e-01 + 8.976985411420985e-01j,
+              4.406101783111199e-01 - 8.976985411420985e-01j,
+              5.539386470258847e-01 + 8.325574907062760e-01j,
+              5.539386470258847e-01 - 8.325574907062760e-01j,
+              6.748464963023645e-01 + 7.379581332490555e-01j,
+              6.748464963023645e-01 - 7.379581332490555e-01j,
+              7.489887970285254e-01 + 6.625826604475596e-01j,
+              7.489887970285254e-01 - 6.625826604475596e-01j,
+              7.913118471618432e-01 + 6.114127579150699e-01j,
+              7.913118471618432e-01 - 6.114127579150699e-01j,
+              7.806804740916381e-01 + 6.249303940216475e-01j,
+              7.806804740916381e-01 - 6.249303940216475e-01j]
+
+        p2 = [-1.025299146693730e-01 + 5.662682444754943e-01j,
+              -1.025299146693730e-01 - 5.662682444754943e-01j,
+               1.698463595163031e-01 + 8.926678667070186e-01j,
+               1.698463595163031e-01 - 8.926678667070186e-01j,
+               2.750532687820631e-01 + 9.351020170094005e-01j,
+               2.750532687820631e-01 - 9.351020170094005e-01j,
+               3.070095178909486e-01 + 9.457373499553291e-01j,
+               3.070095178909486e-01 - 9.457373499553291e-01j,
+               7.695332312152288e-01 + 2.792567212705257e-01j,
+               7.695332312152288e-01 - 2.792567212705257e-01j,
+               8.083818999225620e-01 + 4.990723496863960e-01j,
+               8.083818999225620e-01 - 4.990723496863960e-01j,
+               8.066158014414928e-01 + 5.649811440393374e-01j,
+               8.066158014414928e-01 - 5.649811440393374e-01j,
+               8.062787978834571e-01 + 5.855780880424964e-01j,
+               8.062787978834571e-01 - 5.855780880424964e-01j]
+        k2 = 2.068622545291259e-01
+        assert_allclose(sorted(z, key=np.angle),
+                        sorted(z2, key=np.angle), rtol=1e-6)
+        assert_allclose(sorted(p, key=np.angle),
+                        sorted(p2, key=np.angle), rtol=1e-5)
+        assert_allclose(k, k2, rtol=1e-5)
+
+    def test_ba_output(self):
+        # with transfer function conversion,  without digital conversion
+        b, a = ellip(5, 1, 40, [201, 240], 'stop', True)
+        b2 = [
+             1.000000000000000e+00, 0,  # Matlab: 1.743506051190569e-13,
+             2.426561778314366e+05, 0,  # Matlab: 3.459426536825722e-08,
+             2.348218683400168e+10, 0,  # Matlab: 2.559179747299313e-03,
+             1.132780692872241e+15, 0,  # Matlab: 8.363229375535731e+01,
+             2.724038554089566e+19, 0,  # Matlab: 1.018700994113120e+06,
+             2.612380874940186e+23
+             ]
+        a2 = [
+             1.000000000000000e+00, 1.337266601804649e+02,
+             2.486725353510667e+05, 2.628059713728125e+07,
+             2.436169536928770e+10, 1.913554568577315e+12,
+             1.175208184614438e+15, 6.115751452473410e+16,
+             2.791577695211466e+19, 7.241811142725384e+20,
+             2.612380874940182e+23
+             ]
+        assert_allclose(b, b2, rtol=1e-6)
+        assert_allclose(a, a2, rtol=1e-4)
+
+    def test_fs_param(self):
+        for fs in (900, 900.1, 1234.567):
+            for N in (0, 1, 2, 3, 10):
+                for fc in (100, 100.1, 432.12345):
+                    for btype in ('lp', 'hp'):
+                        ba1 = ellip(N, 1, 20, fc, btype, fs=fs)
+                        ba2 = ellip(N, 1, 20, fc/(fs/2), btype)
+                        assert_allclose(ba1, ba2)
+                for fc in ((100, 200), (100.1, 200.2), (321.123, 432.123)):
+                    for btype in ('bp', 'bs'):
+                        ba1 = ellip(N, 1, 20, fc, btype, fs=fs)
+                        for seq in (list, tuple, array):
+                            fcnorm = seq([f/(fs/2) for f in fc])
+                            ba2 = ellip(N, 1, 20, fcnorm, btype)
+                            assert_allclose(ba1, ba2)
+
+    def test_fs_validation(self):
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            iirnotch(0.06, 30, fs=np.array([10, 20]))
+
+        with pytest.raises(ValueError, match="Sampling.*be none"):
+            iirnotch(0.06, 30, fs=None)
+
+
+def test_sos_consistency():
+    # Consistency checks of output='sos' for the specialized IIR filter
+    # design functions.
+    design_funcs = [(bessel, (0.1,)),
+                    (butter, (0.1,)),
+                    (cheby1, (45.0, 0.1)),
+                    (cheby2, (0.087, 0.1)),
+                    (ellip, (0.087, 45, 0.1))]
+    for func, args in design_funcs:
+        name = func.__name__
+
+        b, a = func(2, *args, output='ba')
+        sos = func(2, *args, output='sos')
+        assert_allclose(sos, [np.hstack((b, a))], err_msg="%s(2,...)" % name)
+
+        zpk = func(3, *args, output='zpk')
+        sos = func(3, *args, output='sos')
+        assert_allclose(sos, zpk2sos(*zpk), err_msg="%s(3,...)" % name)
+
+        zpk = func(4, *args, output='zpk')
+        sos = func(4, *args, output='sos')
+        assert_allclose(sos, zpk2sos(*zpk), err_msg="%s(4,...)" % name)
+
+
+class TestIIRNotch:
+
+    def test_ba_output(self):
+        # Compare coefficients with Matlab ones
+        # for the equivalent input:
+        b, a = iirnotch(0.06, 30)
+        b2 = [
+             9.9686824e-01, -1.9584219e+00,
+             9.9686824e-01
+             ]
+        a2 = [
+             1.0000000e+00, -1.9584219e+00,
+             9.9373647e-01
+             ]
+
+        assert_allclose(b, b2, rtol=1e-8)
+        assert_allclose(a, a2, rtol=1e-8)
+
+    def test_frequency_response(self):
+        # Get filter coefficients
+        b, a = iirnotch(0.3, 30)
+
+        # Get frequency response
+        w, h = freqz(b, a, 1000)
+
+        # Pick 5 point
+        p = [200,  # w0 = 0.200
+             295,  # w0 = 0.295
+             300,  # w0 = 0.300
+             305,  # w0 = 0.305
+             400]  # w0 = 0.400
+
+        # Get frequency response correspondent to each of those points
+        hp = h[p]
+
+        # Check if the frequency response fulfill the specifications:
+        # hp[0] and hp[4]  correspond to frequencies distant from
+        # w0 = 0.3 and should be close to 1
+        assert_allclose(abs(hp[0]), 1, rtol=1e-2)
+        assert_allclose(abs(hp[4]), 1, rtol=1e-2)
+
+        # hp[1] and hp[3] correspond to frequencies approximately
+        # on the edges of the passband and should be close to -3dB
+        assert_allclose(abs(hp[1]), 1/np.sqrt(2), rtol=1e-2)
+        assert_allclose(abs(hp[3]), 1/np.sqrt(2), rtol=1e-2)
+
+        # hp[2] correspond to the frequency that should be removed
+        # the frequency response should be very close to 0
+        assert_allclose(abs(hp[2]), 0, atol=1e-10)
+
+    def test_errors(self):
+        # Exception should be raised if w0 > 1 or w0 <0
+        assert_raises(ValueError, iirnotch, w0=2, Q=30)
+        assert_raises(ValueError, iirnotch, w0=-1, Q=30)
+
+        # Exception should be raised if any of the parameters
+        # are not float (or cannot be converted to one)
+        assert_raises(ValueError, iirnotch, w0="blabla", Q=30)
+        assert_raises(TypeError, iirnotch, w0=-1, Q=[1, 2, 3])
+
+    def test_fs_param(self):
+        # Get filter coefficients
+        b, a = iirnotch(1500, 30, fs=10000)
+
+        # Get frequency response
+        w, h = freqz(b, a, 1000, fs=10000)
+
+        # Pick 5 point
+        p = [200,  # w0 = 1000
+             295,  # w0 = 1475
+             300,  # w0 = 1500
+             305,  # w0 = 1525
+             400]  # w0 = 2000
+
+        # Get frequency response correspondent to each of those points
+        hp = h[p]
+
+        # Check if the frequency response fulfill the specifications:
+        # hp[0] and hp[4]  correspond to frequencies distant from
+        # w0 = 1500 and should be close to 1
+        assert_allclose(abs(hp[0]), 1, rtol=1e-2)
+        assert_allclose(abs(hp[4]), 1, rtol=1e-2)
+
+        # hp[1] and hp[3] correspond to frequencies approximately
+        # on the edges of the passband and should be close to -3dB
+        assert_allclose(abs(hp[1]), 1/np.sqrt(2), rtol=1e-2)
+        assert_allclose(abs(hp[3]), 1/np.sqrt(2), rtol=1e-2)
+
+        # hp[2] correspond to the frequency that should be removed
+        # the frequency response should be very close to 0
+        assert_allclose(abs(hp[2]), 0, atol=1e-10)
+
+
+class TestIIRPeak:
+
+    def test_ba_output(self):
+        # Compare coefficients with Matlab ones
+        # for the equivalent input:
+        b, a = iirpeak(0.06, 30)
+        b2 = [
+             3.131764229e-03, 0,
+             -3.131764229e-03
+             ]
+        a2 = [
+             1.0000000e+00, -1.958421917e+00,
+             9.9373647e-01
+             ]
+        assert_allclose(b, b2, rtol=1e-8)
+        assert_allclose(a, a2, rtol=1e-8)
+
+    def test_frequency_response(self):
+        # Get filter coefficients
+        b, a = iirpeak(0.3, 30)
+
+        # Get frequency response
+        w, h = freqz(b, a, 1000)
+
+        # Pick 5 point
+        p = [30,  # w0 = 0.030
+             295,  # w0 = 0.295
+             300,  # w0 = 0.300
+             305,  # w0 = 0.305
+             800]  # w0 = 0.800
+
+        # Get frequency response correspondent to each of those points
+        hp = h[p]
+
+        # Check if the frequency response fulfill the specifications:
+        # hp[0] and hp[4]  correspond to frequencies distant from
+        # w0 = 0.3 and should be close to 0
+        assert_allclose(abs(hp[0]), 0, atol=1e-2)
+        assert_allclose(abs(hp[4]), 0, atol=1e-2)
+
+        # hp[1] and hp[3] correspond to frequencies approximately
+        # on the edges of the passband and should be close to 10**(-3/20)
+        assert_allclose(abs(hp[1]), 1/np.sqrt(2), rtol=1e-2)
+        assert_allclose(abs(hp[3]), 1/np.sqrt(2), rtol=1e-2)
+
+        # hp[2] correspond to the frequency that should be retained and
+        # the frequency response should be very close to 1
+        assert_allclose(abs(hp[2]), 1, rtol=1e-10)
+
+    def test_errors(self):
+        # Exception should be raised if w0 > 1 or w0 <0
+        assert_raises(ValueError, iirpeak, w0=2, Q=30)
+        assert_raises(ValueError, iirpeak, w0=-1, Q=30)
+
+        # Exception should be raised if any of the parameters
+        # are not float (or cannot be converted to one)
+        assert_raises(ValueError, iirpeak, w0="blabla", Q=30)
+        assert_raises(TypeError, iirpeak, w0=-1, Q=[1, 2, 3])
+
+    def test_fs_param(self):
+        # Get filter coefficients
+        b, a = iirpeak(1200, 30, fs=8000)
+
+        # Get frequency response
+        w, h = freqz(b, a, 1000, fs=8000)
+
+        # Pick 5 point
+        p = [30,  # w0 = 120
+             295,  # w0 = 1180
+             300,  # w0 = 1200
+             305,  # w0 = 1220
+             800]  # w0 = 3200
+
+        # Get frequency response correspondent to each of those points
+        hp = h[p]
+
+        # Check if the frequency response fulfill the specifications:
+        # hp[0] and hp[4]  correspond to frequencies distant from
+        # w0 = 1200 and should be close to 0
+        assert_allclose(abs(hp[0]), 0, atol=1e-2)
+        assert_allclose(abs(hp[4]), 0, atol=1e-2)
+
+        # hp[1] and hp[3] correspond to frequencies approximately
+        # on the edges of the passband and should be close to 10**(-3/20)
+        assert_allclose(abs(hp[1]), 1/np.sqrt(2), rtol=1e-2)
+        assert_allclose(abs(hp[3]), 1/np.sqrt(2), rtol=1e-2)
+
+        # hp[2] correspond to the frequency that should be retained and
+        # the frequency response should be very close to 1
+        assert_allclose(abs(hp[2]), 1, rtol=1e-10)
+
+
+class TestIIRComb:
+    # Test erroneous input cases
+    def test_invalid_input(self):
+        # w0 is <= 0 or >= fs / 2
+        fs = 1000
+        for args in [(-fs, 30), (0, 35), (fs / 2, 40), (fs, 35)]:
+            with pytest.raises(ValueError, match='w0 must be between '):
+                iircomb(*args, fs=fs)
+
+        # fs is not divisible by w0
+        for args in [(120, 30), (157, 35)]:
+            with pytest.raises(ValueError, match='fs must be divisible '):
+                iircomb(*args, fs=fs)
+
+        # https://github.com/scipy/scipy/issues/14043#issuecomment-1107349140
+        # Previously, fs=44100, w0=49.999 was rejected, but fs=2,
+        # w0=49.999/int(44100/2) was accepted. Now it is rejected, too.
+        with pytest.raises(ValueError, match='fs must be divisible '):
+            iircomb(w0=49.999/int(44100/2), Q=30)
+
+        with pytest.raises(ValueError, match='fs must be divisible '):
+            iircomb(w0=49.999, Q=30, fs=44100)
+
+        # Filter type is not notch or peak
+        for args in [(0.2, 30, 'natch'), (0.5, 35, 'comb')]:
+            with pytest.raises(ValueError, match='ftype must be '):
+                iircomb(*args)
+
+    # Verify that the filter's frequency response contains a
+    # notch at the cutoff frequency
+    @pytest.mark.parametrize('ftype', ('notch', 'peak'))
+    def test_frequency_response(self, ftype):
+        # Create a notching or peaking comb filter at 1000 Hz
+        b, a = iircomb(1000, 30, ftype=ftype, fs=10000)
+
+        # Compute the frequency response
+        freqs, response = freqz(b, a, 1000, fs=10000)
+
+        # Find the notch using argrelextrema
+        comb_points = argrelextrema(abs(response), np.less)[0]
+
+        # Verify that the first notch sits at 1000 Hz
+        comb1 = comb_points[0]
+        assert_allclose(freqs[comb1], 1000)
+
+    # Verify pass_zero parameter
+    @pytest.mark.parametrize('ftype,pass_zero,peak,notch',
+                             [('peak', True, 123.45, 61.725),
+                              ('peak', False, 61.725, 123.45),
+                              ('peak', None, 61.725, 123.45),
+                              ('notch', None, 61.725, 123.45),
+                              ('notch', True, 123.45, 61.725),
+                              ('notch', False, 61.725, 123.45)])
+    def test_pass_zero(self, ftype, pass_zero, peak, notch):
+        # Create a notching or peaking comb filter
+        b, a = iircomb(123.45, 30, ftype=ftype, fs=1234.5, pass_zero=pass_zero)
+
+        # Compute the frequency response
+        freqs, response = freqz(b, a, [peak, notch], fs=1234.5)
+
+        # Verify that expected notches are notches and peaks are peaks
+        assert abs(response[0]) > 0.99
+        assert abs(response[1]) < 1e-10
+
+    # All built-in IIR filters are real, so should have perfectly
+    # symmetrical poles and zeros. Then ba representation (using
+    # numpy.poly) will be purely real instead of having negligible
+    # imaginary parts.
+    def test_iir_symmetry(self):
+        b, a = iircomb(400, 30, fs=24000)
+        z, p, k = tf2zpk(b, a)
+        assert_array_equal(sorted(z), sorted(z.conj()))
+        assert_array_equal(sorted(p), sorted(p.conj()))
+        assert_equal(k, np.real(k))
+
+        assert issubclass(b.dtype.type, np.floating)
+        assert issubclass(a.dtype.type, np.floating)
+
+    # Verify filter coefficients with MATLAB's iircomb function
+    def test_ba_output(self):
+        b_notch, a_notch = iircomb(60, 35, ftype='notch', fs=600)
+        b_notch2 = [0.957020174408697, 0.0, 0.0, 0.0, 0.0, 0.0,
+                    0.0, 0.0, 0.0, 0.0, -0.957020174408697]
+        a_notch2 = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+                    0.0, 0.0, 0.0, 0.0, -0.914040348817395]
+        assert_allclose(b_notch, b_notch2)
+        assert_allclose(a_notch, a_notch2)
+
+        b_peak, a_peak = iircomb(60, 35, ftype='peak', fs=600)
+        b_peak2 = [0.0429798255913026, 0.0, 0.0, 0.0, 0.0, 0.0,
+                   0.0, 0.0, 0.0, 0.0, -0.0429798255913026]
+        a_peak2 = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+                   0.0, 0.0, 0.0, 0.0, 0.914040348817395]
+        assert_allclose(b_peak, b_peak2)
+        assert_allclose(a_peak, a_peak2)
+
+    # Verify that https://github.com/scipy/scipy/issues/14043 is fixed
+    def test_nearest_divisor(self):
+        # Create a notching comb filter
+        b, a = iircomb(50/int(44100/2), 50.0, ftype='notch')
+
+        # Compute the frequency response at an upper harmonic of 50
+        freqs, response = freqz(b, a, [22000], fs=44100)
+
+        # Before bug fix, this would produce N = 881, so that 22 kHz was ~0 dB.
+        # Now N = 882 correctly and 22 kHz should be a notch <-220 dB
+        assert abs(response[0]) < 1e-10
+
+    def test_fs_validation(self):
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            iircomb(1000, 30, fs=np.array([10, 20]))
+
+        with pytest.raises(ValueError, match="Sampling.*be none"):
+            iircomb(1000, 30, fs=None)
+
+
+class TestIIRDesign:
+
+    def test_exceptions(self):
+        with pytest.raises(ValueError, match="the same shape"):
+            iirdesign(0.2, [0.1, 0.3], 1, 40)
+        with pytest.raises(ValueError, match="the same shape"):
+            iirdesign(np.array([[0.3, 0.6], [0.3, 0.6]]),
+                      np.array([[0.4, 0.5], [0.4, 0.5]]), 1, 40)
+
+        # discrete filter with non-positive frequency
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign(0, 0.5, 1, 40)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign(-0.1, 0.5, 1, 40)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign(0.1, 0, 1, 40)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign(0.1, -0.5, 1, 40)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign([0, 0.3], [0.1, 0.5], 1, 40)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign([-0.1, 0.3], [0.1, 0.5], 1, 40)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign([0.1, 0], [0.1, 0.5], 1, 40)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign([0.1, -0.3], [0.1, 0.5], 1, 40)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign([0.1, 0.3], [0, 0.5], 1, 40)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign([0.1, 0.3], [-0.1, 0.5], 1, 40)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign([0.1, 0.3], [0.1, 0], 1, 40)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign([0.1, 0.3], [0.1, -0.5], 1, 40)
+
+        # analog filter with negative frequency
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign(-0.1, 0.5, 1, 40, analog=True)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign(0.1, -0.5, 1, 40, analog=True)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign([-0.1, 0.3], [0.1, 0.5], 1, 40, analog=True)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign([0.1, -0.3], [0.1, 0.5], 1, 40, analog=True)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign([0.1, 0.3], [-0.1, 0.5], 1, 40, analog=True)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirdesign([0.1, 0.3], [0.1, -0.5], 1, 40, analog=True)
+
+        # discrete filter with fs=None, freq > 1
+        with pytest.raises(ValueError, match="must be less than 1"):
+            iirdesign(1, 0.5, 1, 40)
+        with pytest.raises(ValueError, match="must be less than 1"):
+            iirdesign(1.1, 0.5, 1, 40)
+        with pytest.raises(ValueError, match="must be less than 1"):
+            iirdesign(0.1, 1, 1, 40)
+        with pytest.raises(ValueError, match="must be less than 1"):
+            iirdesign(0.1, 1.5, 1, 40)
+        with pytest.raises(ValueError, match="must be less than 1"):
+            iirdesign([1, 0.3], [0.1, 0.5], 1, 40)
+        with pytest.raises(ValueError, match="must be less than 1"):
+            iirdesign([1.1, 0.3], [0.1, 0.5], 1, 40)
+        with pytest.raises(ValueError, match="must be less than 1"):
+            iirdesign([0.1, 1], [0.1, 0.5], 1, 40)
+        with pytest.raises(ValueError, match="must be less than 1"):
+            iirdesign([0.1, 1.1], [0.1, 0.5], 1, 40)
+        with pytest.raises(ValueError, match="must be less than 1"):
+            iirdesign([0.1, 0.3], [1, 0.5], 1, 40)
+        with pytest.raises(ValueError, match="must be less than 1"):
+            iirdesign([0.1, 0.3], [1.1, 0.5], 1, 40)
+        with pytest.raises(ValueError, match="must be less than 1"):
+            iirdesign([0.1, 0.3], [0.1, 1], 1, 40)
+        with pytest.raises(ValueError, match="must be less than 1"):
+            iirdesign([0.1, 0.3], [0.1, 1.5], 1, 40)
+
+        # discrete filter with fs>2, wp, ws < fs/2 must pass
+        iirdesign(100, 500, 1, 40, fs=2000)
+        iirdesign(500, 100, 1, 40, fs=2000)
+        iirdesign([200, 400], [100, 500], 1, 40, fs=2000)
+        iirdesign([100, 500], [200, 400], 1, 40, fs=2000)
+
+        # discrete filter with fs>2, freq > fs/2: this must raise
+        with pytest.raises(ValueError, match="must be less than fs/2"):
+            iirdesign(1000, 400, 1, 40, fs=2000)
+        with pytest.raises(ValueError, match="must be less than fs/2"):
+            iirdesign(1100, 500, 1, 40, fs=2000)
+        with pytest.raises(ValueError, match="must be less than fs/2"):
+            iirdesign(100, 1000, 1, 40, fs=2000)
+        with pytest.raises(ValueError, match="must be less than fs/2"):
+            iirdesign(100, 1100, 1, 40, fs=2000)
+        with pytest.raises(ValueError, match="must be less than fs/2"):
+            iirdesign([1000, 400], [100, 500], 1, 40, fs=2000)
+        with pytest.raises(ValueError, match="must be less than fs/2"):
+            iirdesign([1100, 400], [100, 500], 1, 40, fs=2000)
+        with pytest.raises(ValueError, match="must be less than fs/2"):
+            iirdesign([200, 1000], [100, 500], 1, 40, fs=2000)
+        with pytest.raises(ValueError, match="must be less than fs/2"):
+            iirdesign([200, 1100], [100, 500], 1, 40, fs=2000)
+        with pytest.raises(ValueError, match="must be less than fs/2"):
+            iirdesign([200, 400], [1000, 500], 1, 40, fs=2000)
+        with pytest.raises(ValueError, match="must be less than fs/2"):
+            iirdesign([200, 400], [1100, 500], 1, 40, fs=2000)
+        with pytest.raises(ValueError, match="must be less than fs/2"):
+            iirdesign([200, 400], [100, 1000], 1, 40, fs=2000)
+        with pytest.raises(ValueError, match="must be less than fs/2"):
+            iirdesign([200, 400], [100, 1100], 1, 40, fs=2000)
+
+        with pytest.raises(ValueError, match="strictly inside stopband"):
+            iirdesign([0.1, 0.4], [0.5, 0.6], 1, 40)
+        with pytest.raises(ValueError, match="strictly inside stopband"):
+            iirdesign([0.5, 0.6], [0.1, 0.4], 1, 40)
+        with pytest.raises(ValueError, match="strictly inside stopband"):
+            iirdesign([0.3, 0.6], [0.4, 0.7], 1, 40)
+        with pytest.raises(ValueError, match="strictly inside stopband"):
+            iirdesign([0.4, 0.7], [0.3, 0.6], 1, 40)
+
+    def test_fs_validation(self):
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            iirfilter(1, 1, btype="low", fs=np.array([10, 20]))
+
+
+class TestIIRFilter:
+
+    def test_symmetry(self):
+        # All built-in IIR filters are real, so should have perfectly
+        # symmetrical poles and zeros. Then ba representation (using
+        # numpy.poly) will be purely real instead of having negligible
+        # imaginary parts.
+        for N in np.arange(1, 26):
+            for ftype in ('butter', 'bessel', 'cheby1', 'cheby2', 'ellip'):
+                z, p, k = iirfilter(N, 1.1, 1, 20, 'low', analog=True,
+                                    ftype=ftype, output='zpk')
+                assert_array_equal(sorted(z), sorted(z.conj()))
+                assert_array_equal(sorted(p), sorted(p.conj()))
+                assert_equal(k, np.real(k))
+
+                b, a = iirfilter(N, 1.1, 1, 20, 'low', analog=True,
+                                 ftype=ftype, output='ba')
+                assert_(issubclass(b.dtype.type, np.floating))
+                assert_(issubclass(a.dtype.type, np.floating))
+
+    def test_int_inputs(self):
+        # Using integer frequency arguments and large N should not produce
+        # numpy integers that wraparound to negative numbers
+        k = iirfilter(24, 100, btype='low', analog=True, ftype='bessel',
+                      output='zpk')[2]
+        k2 = 9.999999999999989e+47
+        assert_allclose(k, k2)
+        # if fs is specified then the normalization of Wn to have 
+        # 0 <= Wn <= 1 should not cause an integer overflow
+        # the following line should not raise an exception
+        iirfilter(20, [1000000000, 1100000000], btype='bp', 
+                      analog=False, fs=6250000000)
+
+    def test_invalid_wn_size(self):
+        # low and high have 1 Wn, band and stop have 2 Wn
+        assert_raises(ValueError, iirfilter, 1, [0.1, 0.9], btype='low')
+        assert_raises(ValueError, iirfilter, 1, [0.2, 0.5], btype='high')
+        assert_raises(ValueError, iirfilter, 1, 0.2, btype='bp')
+        assert_raises(ValueError, iirfilter, 1, 400, btype='bs', analog=True)
+
+    def test_invalid_wn_range(self):
+        # For digital filters, 0 <= Wn <= 1
+        assert_raises(ValueError, iirfilter, 1, 2, btype='low')
+        assert_raises(ValueError, iirfilter, 1, [0.5, 1], btype='band')
+        assert_raises(ValueError, iirfilter, 1, [0., 0.5], btype='band')
+        assert_raises(ValueError, iirfilter, 1, -1, btype='high')
+        assert_raises(ValueError, iirfilter, 1, [1, 2], btype='band')
+        assert_raises(ValueError, iirfilter, 1, [10, 20], btype='stop')
+
+        # analog=True with non-positive critical frequencies
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirfilter(2, 0, btype='low', analog=True)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirfilter(2, -1, btype='low', analog=True)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirfilter(2, [0, 100], analog=True)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirfilter(2, [-1, 100], analog=True)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirfilter(2, [10, 0], analog=True)
+        with pytest.raises(ValueError, match="must be greater than 0"):
+            iirfilter(2, [10, -1], analog=True)
+
+    def test_analog_sos(self):
+        # first order Butterworth filter with Wn = 1 has tf 1/(s+1)
+        sos = [[0., 0., 1., 0., 1., 1.]]
+        sos2 = iirfilter(N=1, Wn=1, btype='low', analog=True, output='sos')
+        assert_array_almost_equal(sos, sos2)
+
+    def test_wn1_ge_wn0(self):
+        # gh-15773: should raise error if Wn[0] >= Wn[1]
+        with pytest.raises(ValueError,
+                           match=r"Wn\[0\] must be less than Wn\[1\]"):
+            iirfilter(2, [0.5, 0.5])
+        with pytest.raises(ValueError,
+                           match=r"Wn\[0\] must be less than Wn\[1\]"):
+            iirfilter(2, [0.6, 0.5])
+
+
+class TestGroupDelay:
+    def test_identity_filter(self):
+        w, gd = group_delay((1, 1))
+        assert_array_almost_equal(w, pi * np.arange(512) / 512)
+        assert_array_almost_equal(gd, np.zeros(512))
+        w, gd = group_delay((1, 1), whole=True)
+        assert_array_almost_equal(w, 2 * pi * np.arange(512) / 512)
+        assert_array_almost_equal(gd, np.zeros(512))
+
+    def test_fir(self):
+        # Let's design linear phase FIR and check that the group delay
+        # is constant.
+        N = 100
+        b = firwin(N + 1, 0.1)
+        w, gd = group_delay((b, 1))
+        assert_allclose(gd, 0.5 * N)
+
+    def test_iir(self):
+        # Let's design Butterworth filter and test the group delay at
+        # some points against MATLAB answer.
+        b, a = butter(4, 0.1)
+        w = np.linspace(0, pi, num=10, endpoint=False)
+        w, gd = group_delay((b, a), w=w)
+        matlab_gd = np.array([8.249313898506037, 11.958947880907104,
+                              2.452325615326005, 1.048918665702008,
+                              0.611382575635897, 0.418293269460578,
+                              0.317932917836572, 0.261371844762525,
+                              0.229038045801298, 0.212185774208521])
+        assert_array_almost_equal(gd, matlab_gd)
+
+    def test_singular(self):
+        # Let's create a filter with zeros and poles on the unit circle and
+        # check if warnings are raised at those frequencies.
+        z1 = np.exp(1j * 0.1 * pi)
+        z2 = np.exp(1j * 0.25 * pi)
+        p1 = np.exp(1j * 0.5 * pi)
+        p2 = np.exp(1j * 0.8 * pi)
+        b = np.convolve([1, -z1], [1, -z2])
+        a = np.convolve([1, -p1], [1, -p2])
+        w = np.array([0.1 * pi, 0.25 * pi, -0.5 * pi, -0.8 * pi])
+
+        w, gd = assert_warns(UserWarning, group_delay, (b, a), w=w)
+
+    def test_backward_compat(self):
+        # For backward compatibility, test if None act as a wrapper for default
+        w1, gd1 = group_delay((1, 1))
+        w2, gd2 = group_delay((1, 1), None)
+        assert_array_almost_equal(w1, w2)
+        assert_array_almost_equal(gd1, gd2)
+
+    def test_fs_param(self):
+        # Let's design Butterworth filter and test the group delay at
+        # some points against the normalized frequency answer.
+        b, a = butter(4, 4800, fs=96000)
+        w = np.linspace(0, 96000/2, num=10, endpoint=False)
+        w, gd = group_delay((b, a), w=w, fs=96000)
+        norm_gd = np.array([8.249313898506037, 11.958947880907104,
+                            2.452325615326005, 1.048918665702008,
+                            0.611382575635897, 0.418293269460578,
+                            0.317932917836572, 0.261371844762525,
+                            0.229038045801298, 0.212185774208521])
+        assert_array_almost_equal(gd, norm_gd)
+
+    def test_w_or_N_types(self):
+        # Measure at 8 equally-spaced points
+        for N in (8, np.int8(8), np.int16(8), np.int32(8), np.int64(8),
+                  np.array(8)):
+            w, gd = group_delay((1, 1), N)
+            assert_array_almost_equal(w, pi * np.arange(8) / 8)
+            assert_array_almost_equal(gd, np.zeros(8))
+
+        # Measure at frequency 8 rad/sec
+        for w in (8.0, 8.0+0j):
+            w_out, gd = group_delay((1, 1), w)
+            assert_array_almost_equal(w_out, [8])
+            assert_array_almost_equal(gd, [0])
+
+    def test_complex_coef(self):
+        # gh-19586: handle complex coef TFs
+        #
+        # for g(z) = (alpha*z+1)/(1+conjugate(alpha)), group delay is
+        # given by function below.
+        #
+        # def gd_expr(w, alpha):
+        #     num = 1j*(abs(alpha)**2-1)*np.exp(1j*w)
+        #     den = (alpha*np.exp(1j*w)+1)*(np.exp(1j*w)+np.conj(alpha))
+        #     return -np.imag(num/den)
+
+        # arbitrary non-real alpha
+        alpha = -0.6143077933232609+0.3355978770229421j
+        # 8 points from from -pi to pi
+        wref = np.array([-3.141592653589793 ,
+                         -2.356194490192345 ,
+                         -1.5707963267948966,
+                         -0.7853981633974483,
+                         0.                ,
+                         0.7853981633974483,
+                         1.5707963267948966,
+                         2.356194490192345 ])
+        gdref =  array([0.18759548150354619,
+                        0.17999770352712252,
+                        0.23598047471879877,
+                        0.46539443069907194,
+                        1.9511492420564165 ,
+                        3.478129975138865  ,
+                        0.6228594960517333 ,
+                        0.27067831839471224])
+        b = [alpha,1]
+        a = [1, np.conjugate(alpha)]
+        gdtest = group_delay((b,a), wref)[1]
+        # need nulp=14 for macOS arm64 wheel builds; added 2 for some
+        # robustness on other platforms.
+        assert_array_almost_equal_nulp(gdtest, gdref, nulp=16)
+
+    def test_fs_validation(self):
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            group_delay((1, 1), fs=np.array([10, 20]))
+
+        with pytest.raises(ValueError, match="Sampling.*be none"):
+            group_delay((1, 1), fs=None)
+
+
+class TestGammatone:
+    # Test erroneous input cases.
+    def test_invalid_input(self):
+        # Cutoff frequency is <= 0 or >= fs / 2.
+        fs = 16000
+        for args in [(-fs, 'iir'), (0, 'fir'), (fs / 2, 'iir'), (fs, 'fir')]:
+            with pytest.raises(ValueError, match='The frequency must be '
+                               'between '):
+                gammatone(*args, fs=fs)
+
+        # Filter type is not fir or iir
+        for args in [(440, 'fie'), (220, 'it')]:
+            with pytest.raises(ValueError, match='ftype must be '):
+                gammatone(*args, fs=fs)
+
+        # Order is <= 0 or > 24 for FIR filter.
+        for args in [(440, 'fir', -50), (220, 'fir', 0), (110, 'fir', 25),
+                     (55, 'fir', 50)]:
+            with pytest.raises(ValueError, match='Invalid order: '):
+                gammatone(*args, numtaps=None, fs=fs)
+
+    # Verify that the filter's frequency response is approximately
+    # 1 at the cutoff frequency.
+    def test_frequency_response(self):
+        fs = 16000
+        ftypes = ['fir', 'iir']
+        for ftype in ftypes:
+            # Create a gammatone filter centered at 1000 Hz.
+            b, a = gammatone(1000, ftype, fs=fs)
+
+            # Calculate the frequency response.
+            freqs, response = freqz(b, a)
+
+            # Determine peak magnitude of the response
+            # and corresponding frequency.
+            response_max = np.max(np.abs(response))
+            freq_hz = freqs[np.argmax(np.abs(response))] / ((2 * np.pi) / fs)
+
+            # Check that the peak magnitude is 1 and the frequency is 1000 Hz.
+            assert_allclose(response_max, 1, rtol=1e-2)
+            assert_allclose(freq_hz, 1000, rtol=1e-2)
+
+    # All built-in IIR filters are real, so should have perfectly
+    # symmetrical poles and zeros. Then ba representation (using
+    # numpy.poly) will be purely real instead of having negligible
+    # imaginary parts.
+    def test_iir_symmetry(self):
+        b, a = gammatone(440, 'iir', fs=24000)
+        z, p, k = tf2zpk(b, a)
+        assert_array_equal(sorted(z), sorted(z.conj()))
+        assert_array_equal(sorted(p), sorted(p.conj()))
+        assert_equal(k, np.real(k))
+
+        assert_(issubclass(b.dtype.type, np.floating))
+        assert_(issubclass(a.dtype.type, np.floating))
+
+    # Verify FIR filter coefficients with the paper's
+    # Mathematica implementation
+    def test_fir_ba_output(self):
+        b, _ = gammatone(15, 'fir', fs=1000)
+        b2 = [0.0, 2.2608075649884e-04,
+              1.5077903981357e-03, 4.2033687753998e-03,
+              8.1508962726503e-03, 1.2890059089154e-02,
+              1.7833890391666e-02, 2.2392613558564e-02,
+              2.6055195863104e-02, 2.8435872863284e-02,
+              2.9293319149544e-02, 2.852976858014e-02,
+              2.6176557156294e-02, 2.2371510270395e-02,
+              1.7332485267759e-02]
+        assert_allclose(b, b2)
+
+    # Verify IIR filter coefficients with the paper's MATLAB implementation
+    def test_iir_ba_output(self):
+        b, a = gammatone(440, 'iir', fs=16000)
+        b2 = [1.31494461367464e-06, -5.03391196645395e-06,
+              7.00649426000897e-06, -4.18951968419854e-06,
+              9.02614910412011e-07]
+        a2 = [1.0, -7.65646235454218,
+              25.7584699322366, -49.7319214483238,
+              60.2667361289181, -46.9399590980486,
+              22.9474798808461, -6.43799381299034,
+              0.793651554625368]
+        assert_allclose(b, b2)
+        assert_allclose(a, a2)
+
+    def test_fs_validation(self):
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            gammatone(440, 'iir', fs=np.array([10, 20]))
+
+
+class TestOrderFilter:
+    def test_doc_example(self):
+        x = np.arange(25).reshape(5, 5)
+        domain = np.identity(3)
+
+        # minimum of elements 1,3,9 (zero-padded) on phone pad
+        # 7,5,3 on numpad
+        expected = np.array(
+            [[0., 0., 0., 0., 0.],
+             [0., 0., 1., 2., 0.],
+             [0., 5., 6., 7., 0.],
+             [0., 10., 11., 12., 0.],
+             [0., 0., 0., 0., 0.]],
+        )
+        assert_allclose(order_filter(x, domain, 0), expected)
+
+        # maximum of elements 1,3,9 (zero-padded) on phone pad
+        # 7,5,3 on numpad
+        expected = np.array(
+            [[6., 7., 8., 9., 4.],
+             [11., 12., 13., 14., 9.],
+             [16., 17., 18., 19., 14.],
+             [21., 22., 23., 24., 19.],
+             [20., 21., 22., 23., 24.]],
+        )
+        assert_allclose(order_filter(x, domain, 2), expected)
+
+        # and, just to complete the set, median of zero-padded elements
+        expected = np.array(
+            [[0, 1, 2, 3, 0],
+             [5, 6, 7, 8, 3],
+             [10, 11, 12, 13, 8],
+             [15, 16, 17, 18, 13],
+             [0, 15, 16, 17, 18]],
+        )
+        assert_allclose(order_filter(x, domain, 1), expected)
+
+    def test_medfilt_order_filter(self):
+        x = np.arange(25).reshape(5, 5)
+
+        # median of zero-padded elements 1,5,9 on phone pad
+        # 7,5,3 on numpad
+        expected = np.array(
+            [[0, 1, 2, 3, 0],
+             [1, 6, 7, 8, 4],
+             [6, 11, 12, 13, 9],
+             [11, 16, 17, 18, 14],
+             [0, 16, 17, 18, 0]],
+        )
+        assert_allclose(medfilt(x, 3), expected)
+
+        assert_allclose(
+            order_filter(x, np.ones((3, 3)), 4),
+            expected
+        )
+
+    def test_order_filter_asymmetric(self):
+        x = np.arange(25).reshape(5, 5)
+        domain = np.array(
+            [[1, 1, 0],
+             [0, 1, 0],
+             [0, 0, 0]],
+        )
+
+        expected = np.array(
+            [[0, 0, 0, 0, 0],
+             [0, 0, 1, 2, 3],
+             [0, 5, 6, 7, 8],
+             [0, 10, 11, 12, 13],
+             [0, 15, 16, 17, 18]]
+        )
+        assert_allclose(order_filter(x, domain, 0), expected)
+
+        expected = np.array(
+            [[0, 0, 0, 0, 0],
+             [0, 1, 2, 3, 4],
+             [5, 6, 7, 8, 9],
+             [10, 11, 12, 13, 14],
+             [15, 16, 17, 18, 19]]
+        )
+        assert_allclose(order_filter(x, domain, 1), expected)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_fir_filter_design.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_fir_filter_design.py
new file mode 100644
index 0000000000000000000000000000000000000000..d9fc229829657491d84f35510b34731b055a360f
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_fir_filter_design.py
@@ -0,0 +1,647 @@
+import numpy as np
+from numpy.testing import (assert_almost_equal, assert_array_almost_equal,
+                           assert_equal, assert_,
+                           assert_allclose, assert_warns)
+from pytest import raises as assert_raises
+import pytest
+
+from scipy.fft import fft
+from scipy.special import sinc
+from scipy.signal import kaiser_beta, kaiser_atten, kaiserord, \
+    firwin, firwin2, freqz, remez, firls, minimum_phase
+
+
+def test_kaiser_beta():
+    b = kaiser_beta(58.7)
+    assert_almost_equal(b, 0.1102 * 50.0)
+    b = kaiser_beta(22.0)
+    assert_almost_equal(b, 0.5842 + 0.07886)
+    b = kaiser_beta(21.0)
+    assert_equal(b, 0.0)
+    b = kaiser_beta(10.0)
+    assert_equal(b, 0.0)
+
+
+def test_kaiser_atten():
+    a = kaiser_atten(1, 1.0)
+    assert_equal(a, 7.95)
+    a = kaiser_atten(2, 1/np.pi)
+    assert_equal(a, 2.285 + 7.95)
+
+
+def test_kaiserord():
+    assert_raises(ValueError, kaiserord, 1.0, 1.0)
+    numtaps, beta = kaiserord(2.285 + 7.95 - 0.001, 1/np.pi)
+    assert_equal((numtaps, beta), (2, 0.0))
+
+
+class TestFirwin:
+
+    def check_response(self, h, expected_response, tol=.05):
+        N = len(h)
+        alpha = 0.5 * (N-1)
+        m = np.arange(0,N) - alpha   # time indices of taps
+        for freq, expected in expected_response:
+            actual = abs(np.sum(h*np.exp(-1.j*np.pi*m*freq)))
+            mse = abs(actual-expected)**2
+            assert_(mse < tol, f'response not as expected, mse={mse:g} > {tol:g}')
+
+    def test_response(self):
+        N = 51
+        f = .5
+        # increase length just to try even/odd
+        h = firwin(N, f)  # low-pass from 0 to f
+        self.check_response(h, [(.25,1), (.75,0)])
+
+        h = firwin(N+1, f, window='nuttall')  # specific window
+        self.check_response(h, [(.25,1), (.75,0)])
+
+        h = firwin(N+2, f, pass_zero=False)  # stop from 0 to f --> high-pass
+        self.check_response(h, [(.25,0), (.75,1)])
+
+        f1, f2, f3, f4 = .2, .4, .6, .8
+        h = firwin(N+3, [f1, f2], pass_zero=False)  # band-pass filter
+        self.check_response(h, [(.1,0), (.3,1), (.5,0)])
+
+        h = firwin(N+4, [f1, f2])  # band-stop filter
+        self.check_response(h, [(.1,1), (.3,0), (.5,1)])
+
+        h = firwin(N+5, [f1, f2, f3, f4], pass_zero=False, scale=False)
+        self.check_response(h, [(.1,0), (.3,1), (.5,0), (.7,1), (.9,0)])
+
+        h = firwin(N+6, [f1, f2, f3, f4])  # multiband filter
+        self.check_response(h, [(.1,1), (.3,0), (.5,1), (.7,0), (.9,1)])
+
+        h = firwin(N+7, 0.1, width=.03)  # low-pass
+        self.check_response(h, [(.05,1), (.75,0)])
+
+        h = firwin(N+8, 0.1, pass_zero=False)  # high-pass
+        self.check_response(h, [(.05,0), (.75,1)])
+
+    def mse(self, h, bands):
+        """Compute mean squared error versus ideal response across frequency
+        band.
+          h -- coefficients
+          bands -- list of (left, right) tuples relative to 1==Nyquist of
+            passbands
+        """
+        w, H = freqz(h, worN=1024)
+        f = w/np.pi
+        passIndicator = np.zeros(len(w), bool)
+        for left, right in bands:
+            passIndicator |= (f >= left) & (f < right)
+        Hideal = np.where(passIndicator, 1, 0)
+        mse = np.mean(abs(abs(H)-Hideal)**2)
+        return mse
+
+    def test_scaling(self):
+        """
+        For one lowpass, bandpass, and highpass example filter, this test
+        checks two things:
+          - the mean squared error over the frequency domain of the unscaled
+            filter is smaller than the scaled filter (true for rectangular
+            window)
+          - the response of the scaled filter is exactly unity at the center
+            of the first passband
+        """
+        N = 11
+        cases = [
+            ([.5], True, (0, 1)),
+            ([0.2, .6], False, (.4, 1)),
+            ([.5], False, (1, 1)),
+        ]
+        for cutoff, pass_zero, expected_response in cases:
+            h = firwin(N, cutoff, scale=False, pass_zero=pass_zero, window='ones')
+            hs = firwin(N, cutoff, scale=True, pass_zero=pass_zero, window='ones')
+            if len(cutoff) == 1:
+                if pass_zero:
+                    cutoff = [0] + cutoff
+                else:
+                    cutoff = cutoff + [1]
+            assert_(self.mse(h, [cutoff]) < self.mse(hs, [cutoff]),
+                'least squares violation')
+            self.check_response(hs, [expected_response], 1e-12)
+
+    def test_fs_validation(self):
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            firwin(51, .5, fs=np.array([10, 20]))
+
+
+class TestFirWinMore:
+    """Different author, different style, different tests..."""
+
+    def test_lowpass(self):
+        width = 0.04
+        ntaps, beta = kaiserord(120, width)
+        kwargs = dict(cutoff=0.5, window=('kaiser', beta), scale=False)
+        taps = firwin(ntaps, **kwargs)
+
+        # Check the symmetry of taps.
+        assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
+
+        # Check the gain at a few samples where
+        # we know it should be approximately 0 or 1.
+        freq_samples = np.array([0.0, 0.25, 0.5-width/2, 0.5+width/2, 0.75, 1.0])
+        freqs, response = freqz(taps, worN=np.pi*freq_samples)
+        assert_array_almost_equal(np.abs(response),
+                                    [1.0, 1.0, 1.0, 0.0, 0.0, 0.0], decimal=5)
+
+        taps_str = firwin(ntaps, pass_zero='lowpass', **kwargs)
+        assert_allclose(taps, taps_str)
+
+    def test_highpass(self):
+        width = 0.04
+        ntaps, beta = kaiserord(120, width)
+
+        # Ensure that ntaps is odd.
+        ntaps |= 1
+
+        kwargs = dict(cutoff=0.5, window=('kaiser', beta), scale=False)
+        taps = firwin(ntaps, pass_zero=False, **kwargs)
+
+        # Check the symmetry of taps.
+        assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
+
+        # Check the gain at a few samples where
+        # we know it should be approximately 0 or 1.
+        freq_samples = np.array([0.0, 0.25, 0.5-width/2, 0.5+width/2, 0.75, 1.0])
+        freqs, response = freqz(taps, worN=np.pi*freq_samples)
+        assert_array_almost_equal(np.abs(response),
+                                    [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], decimal=5)
+
+        taps_str = firwin(ntaps, pass_zero='highpass', **kwargs)
+        assert_allclose(taps, taps_str)
+
+    def test_bandpass(self):
+        width = 0.04
+        ntaps, beta = kaiserord(120, width)
+        kwargs = dict(cutoff=[0.3, 0.7], window=('kaiser', beta), scale=False)
+        taps = firwin(ntaps, pass_zero=False, **kwargs)
+
+        # Check the symmetry of taps.
+        assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
+
+        # Check the gain at a few samples where
+        # we know it should be approximately 0 or 1.
+        freq_samples = np.array([0.0, 0.2, 0.3-width/2, 0.3+width/2, 0.5,
+                                0.7-width/2, 0.7+width/2, 0.8, 1.0])
+        freqs, response = freqz(taps, worN=np.pi*freq_samples)
+        assert_array_almost_equal(np.abs(response),
+                [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0], decimal=5)
+
+        taps_str = firwin(ntaps, pass_zero='bandpass', **kwargs)
+        assert_allclose(taps, taps_str)
+
+    def test_bandstop_multi(self):
+        width = 0.04
+        ntaps, beta = kaiserord(120, width)
+        kwargs = dict(cutoff=[0.2, 0.5, 0.8], window=('kaiser', beta),
+                      scale=False)
+        taps = firwin(ntaps, **kwargs)
+
+        # Check the symmetry of taps.
+        assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
+
+        # Check the gain at a few samples where
+        # we know it should be approximately 0 or 1.
+        freq_samples = np.array([0.0, 0.1, 0.2-width/2, 0.2+width/2, 0.35,
+                                0.5-width/2, 0.5+width/2, 0.65,
+                                0.8-width/2, 0.8+width/2, 0.9, 1.0])
+        freqs, response = freqz(taps, worN=np.pi*freq_samples)
+        assert_array_almost_equal(np.abs(response),
+                [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0],
+                decimal=5)
+
+        taps_str = firwin(ntaps, pass_zero='bandstop', **kwargs)
+        assert_allclose(taps, taps_str)
+
+    def test_fs_nyq(self):
+        """Test the fs and nyq keywords."""
+        nyquist = 1000
+        width = 40.0
+        relative_width = width/nyquist
+        ntaps, beta = kaiserord(120, relative_width)
+        taps = firwin(ntaps, cutoff=[300, 700], window=('kaiser', beta),
+                        pass_zero=False, scale=False, fs=2*nyquist)
+
+        # Check the symmetry of taps.
+        assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
+
+        # Check the gain at a few samples where
+        # we know it should be approximately 0 or 1.
+        freq_samples = np.array([0.0, 200, 300-width/2, 300+width/2, 500,
+                                700-width/2, 700+width/2, 800, 1000])
+        freqs, response = freqz(taps, worN=np.pi*freq_samples/nyquist)
+        assert_array_almost_equal(np.abs(response),
+                [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0], decimal=5)
+
+    def test_bad_cutoff(self):
+        """Test that invalid cutoff argument raises ValueError."""
+        # cutoff values must be greater than 0 and less than 1.
+        assert_raises(ValueError, firwin, 99, -0.5)
+        assert_raises(ValueError, firwin, 99, 1.5)
+        # Don't allow 0 or 1 in cutoff.
+        assert_raises(ValueError, firwin, 99, [0, 0.5])
+        assert_raises(ValueError, firwin, 99, [0.5, 1])
+        # cutoff values must be strictly increasing.
+        assert_raises(ValueError, firwin, 99, [0.1, 0.5, 0.2])
+        assert_raises(ValueError, firwin, 99, [0.1, 0.5, 0.5])
+        # Must have at least one cutoff value.
+        assert_raises(ValueError, firwin, 99, [])
+        # 2D array not allowed.
+        assert_raises(ValueError, firwin, 99, [[0.1, 0.2],[0.3, 0.4]])
+        # cutoff values must be less than nyq.
+        assert_raises(ValueError, firwin, 99, 50.0, fs=80)
+        assert_raises(ValueError, firwin, 99, [10, 20, 30], fs=50)
+
+    def test_even_highpass_raises_value_error(self):
+        """Test that attempt to create a highpass filter with an even number
+        of taps raises a ValueError exception."""
+        assert_raises(ValueError, firwin, 40, 0.5, pass_zero=False)
+        assert_raises(ValueError, firwin, 40, [.25, 0.5])
+
+    def test_bad_pass_zero(self):
+        """Test degenerate pass_zero cases."""
+        with assert_raises(ValueError, match='pass_zero must be'):
+            firwin(41, 0.5, pass_zero='foo')
+        with assert_raises(TypeError, match='cannot be interpreted'):
+            firwin(41, 0.5, pass_zero=1.)
+        for pass_zero in ('lowpass', 'highpass'):
+            with assert_raises(ValueError, match='cutoff must have one'):
+                firwin(41, [0.5, 0.6], pass_zero=pass_zero)
+        for pass_zero in ('bandpass', 'bandstop'):
+            with assert_raises(ValueError, match='must have at least two'):
+                firwin(41, [0.5], pass_zero=pass_zero)
+
+    def test_fs_validation(self):
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            firwin2(51, .5, 1, fs=np.array([10, 20]))
+
+
+class TestFirwin2:
+
+    def test_invalid_args(self):
+        # `freq` and `gain` have different lengths.
+        with assert_raises(ValueError, match='must be of same length'):
+            firwin2(50, [0, 0.5, 1], [0.0, 1.0])
+        # `nfreqs` is less than `ntaps`.
+        with assert_raises(ValueError, match='ntaps must be less than nfreqs'):
+            firwin2(50, [0, 0.5, 1], [0.0, 1.0, 1.0], nfreqs=33)
+        # Decreasing value in `freq`
+        with assert_raises(ValueError, match='must be nondecreasing'):
+            firwin2(50, [0, 0.5, 0.4, 1.0], [0, .25, .5, 1.0])
+        # Value in `freq` repeated more than once.
+        with assert_raises(ValueError, match='must not occur more than twice'):
+            firwin2(50, [0, .1, .1, .1, 1.0], [0.0, 0.5, 0.75, 1.0, 1.0])
+        # `freq` does not start at 0.0.
+        with assert_raises(ValueError, match='start with 0'):
+            firwin2(50, [0.5, 1.0], [0.0, 1.0])
+        # `freq` does not end at fs/2.
+        with assert_raises(ValueError, match='end with fs/2'):
+            firwin2(50, [0.0, 0.5], [0.0, 1.0])
+        # Value 0 is repeated in `freq`
+        with assert_raises(ValueError, match='0 must not be repeated'):
+            firwin2(50, [0.0, 0.0, 0.5, 1.0], [1.0, 1.0, 0.0, 0.0])
+        # Value fs/2 is repeated in `freq`
+        with assert_raises(ValueError, match='fs/2 must not be repeated'):
+            firwin2(50, [0.0, 0.5, 1.0, 1.0], [1.0, 1.0, 0.0, 0.0])
+        # Value in `freq` that is too close to a repeated number
+        with assert_raises(ValueError, match='cannot contain numbers '
+                                             'that are too close'):
+            firwin2(50, [0.0, 0.5 - np.finfo(float).eps * 0.5, 0.5, 0.5, 1.0],
+                        [1.0, 1.0, 1.0, 0.0, 0.0])
+
+        # Type II filter, but the gain at nyquist frequency is not zero.
+        with assert_raises(ValueError, match='Type II filter'):
+            firwin2(16, [0.0, 0.5, 1.0], [0.0, 1.0, 1.0])
+
+        # Type III filter, but the gains at nyquist and zero rate are not zero.
+        with assert_raises(ValueError, match='Type III filter'):
+            firwin2(17, [0.0, 0.5, 1.0], [0.0, 1.0, 1.0], antisymmetric=True)
+        with assert_raises(ValueError, match='Type III filter'):
+            firwin2(17, [0.0, 0.5, 1.0], [1.0, 1.0, 0.0], antisymmetric=True)
+        with assert_raises(ValueError, match='Type III filter'):
+            firwin2(17, [0.0, 0.5, 1.0], [1.0, 1.0, 1.0], antisymmetric=True)
+
+        # Type IV filter, but the gain at zero rate is not zero.
+        with assert_raises(ValueError, match='Type IV filter'):
+            firwin2(16, [0.0, 0.5, 1.0], [1.0, 1.0, 0.0], antisymmetric=True)
+
+    def test01(self):
+        width = 0.04
+        beta = 12.0
+        ntaps = 400
+        # Filter is 1 from w=0 to w=0.5, then decreases linearly from 1 to 0 as w
+        # increases from w=0.5 to w=1  (w=1 is the Nyquist frequency).
+        freq = [0.0, 0.5, 1.0]
+        gain = [1.0, 1.0, 0.0]
+        taps = firwin2(ntaps, freq, gain, window=('kaiser', beta))
+        freq_samples = np.array([0.0, 0.25, 0.5-width/2, 0.5+width/2,
+                                                        0.75, 1.0-width/2])
+        freqs, response = freqz(taps, worN=np.pi*freq_samples)
+        assert_array_almost_equal(np.abs(response),
+                        [1.0, 1.0, 1.0, 1.0-width, 0.5, width], decimal=5)
+
+    def test02(self):
+        width = 0.04
+        beta = 12.0
+        # ntaps must be odd for positive gain at Nyquist.
+        ntaps = 401
+        # An ideal highpass filter.
+        freq = [0.0, 0.5, 0.5, 1.0]
+        gain = [0.0, 0.0, 1.0, 1.0]
+        taps = firwin2(ntaps, freq, gain, window=('kaiser', beta))
+        freq_samples = np.array([0.0, 0.25, 0.5-width, 0.5+width, 0.75, 1.0])
+        freqs, response = freqz(taps, worN=np.pi*freq_samples)
+        assert_array_almost_equal(np.abs(response),
+                                [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], decimal=5)
+
+    def test03(self):
+        width = 0.02
+        ntaps, beta = kaiserord(120, width)
+        # ntaps must be odd for positive gain at Nyquist.
+        ntaps = int(ntaps) | 1
+        freq = [0.0, 0.4, 0.4, 0.5, 0.5, 1.0]
+        gain = [1.0, 1.0, 0.0, 0.0, 1.0, 1.0]
+        taps = firwin2(ntaps, freq, gain, window=('kaiser', beta))
+        freq_samples = np.array([0.0, 0.4-width, 0.4+width, 0.45,
+                                    0.5-width, 0.5+width, 0.75, 1.0])
+        freqs, response = freqz(taps, worN=np.pi*freq_samples)
+        assert_array_almost_equal(np.abs(response),
+                    [1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], decimal=5)
+
+    def test04(self):
+        """Test firwin2 when window=None."""
+        ntaps = 5
+        # Ideal lowpass: gain is 1 on [0,0.5], and 0 on [0.5, 1.0]
+        freq = [0.0, 0.5, 0.5, 1.0]
+        gain = [1.0, 1.0, 0.0, 0.0]
+        taps = firwin2(ntaps, freq, gain, window=None, nfreqs=8193)
+        alpha = 0.5 * (ntaps - 1)
+        m = np.arange(0, ntaps) - alpha
+        h = 0.5 * sinc(0.5 * m)
+        assert_array_almost_equal(h, taps)
+
+    def test05(self):
+        """Test firwin2 for calculating Type IV filters"""
+        ntaps = 1500
+
+        freq = [0.0, 1.0]
+        gain = [0.0, 1.0]
+        taps = firwin2(ntaps, freq, gain, window=None, antisymmetric=True)
+        assert_array_almost_equal(taps[: ntaps // 2], -taps[ntaps // 2:][::-1])
+
+        freqs, response = freqz(taps, worN=2048)
+        assert_array_almost_equal(abs(response), freqs / np.pi, decimal=4)
+
+    def test06(self):
+        """Test firwin2 for calculating Type III filters"""
+        ntaps = 1501
+
+        freq = [0.0, 0.5, 0.55, 1.0]
+        gain = [0.0, 0.5, 0.0, 0.0]
+        taps = firwin2(ntaps, freq, gain, window=None, antisymmetric=True)
+        assert_equal(taps[ntaps // 2], 0.0)
+        assert_array_almost_equal(taps[: ntaps // 2], -taps[ntaps // 2 + 1:][::-1])
+
+        freqs, response1 = freqz(taps, worN=2048)
+        response2 = np.interp(freqs / np.pi, freq, gain)
+        assert_array_almost_equal(abs(response1), response2, decimal=3)
+
+    def test_fs_nyq(self):
+        taps1 = firwin2(80, [0.0, 0.5, 1.0], [1.0, 1.0, 0.0])
+        taps2 = firwin2(80, [0.0, 30.0, 60.0], [1.0, 1.0, 0.0], fs=120.0)
+        assert_array_almost_equal(taps1, taps2)
+
+    def test_tuple(self):
+        taps1 = firwin2(150, (0.0, 0.5, 0.5, 1.0), (1.0, 1.0, 0.0, 0.0))
+        taps2 = firwin2(150, [0.0, 0.5, 0.5, 1.0], [1.0, 1.0, 0.0, 0.0])
+        assert_array_almost_equal(taps1, taps2)
+
+    def test_input_modyfication(self):
+        freq1 = np.array([0.0, 0.5, 0.5, 1.0])
+        freq2 = np.array(freq1)
+        firwin2(80, freq1, [1.0, 1.0, 0.0, 0.0])
+        assert_equal(freq1, freq2)
+
+
+class TestRemez:
+
+    def test_bad_args(self):
+        assert_raises(ValueError, remez, 11, [0.1, 0.4], [1], type='pooka')
+
+    def test_hilbert(self):
+        N = 11  # number of taps in the filter
+        a = 0.1  # width of the transition band
+
+        # design an unity gain hilbert bandpass filter from w to 0.5-w
+        h = remez(11, [a, 0.5-a], [1], type='hilbert')
+
+        # make sure the filter has correct # of taps
+        assert_(len(h) == N, "Number of Taps")
+
+        # make sure it is type III (anti-symmetric tap coefficients)
+        assert_array_almost_equal(h[:(N-1)//2], -h[:-(N-1)//2-1:-1])
+
+        # Since the requested response is symmetric, all even coefficients
+        # should be zero (or in this case really small)
+        assert_((abs(h[1::2]) < 1e-15).all(), "Even Coefficients Equal Zero")
+
+        # now check the frequency response
+        w, H = freqz(h, 1)
+        f = w/2/np.pi
+        Hmag = abs(H)
+
+        # should have a zero at 0 and pi (in this case close to zero)
+        assert_((Hmag[[0, -1]] < 0.02).all(), "Zero at zero and pi")
+
+        # check that the pass band is close to unity
+        idx = np.logical_and(f > a, f < 0.5-a)
+        assert_((abs(Hmag[idx] - 1) < 0.015).all(), "Pass Band Close To Unity")
+
+    def test_compare(self):
+        # test comparison to MATLAB
+        k = [0.024590270518440, -0.041314581814658, -0.075943803756711,
+             -0.003530911231040, 0.193140296954975, 0.373400753484939,
+             0.373400753484939, 0.193140296954975, -0.003530911231040,
+             -0.075943803756711, -0.041314581814658, 0.024590270518440]
+        h = remez(12, [0, 0.3, 0.5, 1], [1, 0], fs=2.)
+        assert_allclose(h, k)
+
+        h = [-0.038976016082299, 0.018704846485491, -0.014644062687875,
+             0.002879152556419, 0.016849978528150, -0.043276706138248,
+             0.073641298245579, -0.103908158578635, 0.129770906801075,
+             -0.147163447297124, 0.153302248456347, -0.147163447297124,
+             0.129770906801075, -0.103908158578635, 0.073641298245579,
+             -0.043276706138248, 0.016849978528150, 0.002879152556419,
+             -0.014644062687875, 0.018704846485491, -0.038976016082299]
+        assert_allclose(remez(21, [0, 0.8, 0.9, 1], [0, 1], fs=2.), h)
+
+    def test_fs_validation(self):
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            remez(11, .1, 1, fs=np.array([10, 20]))
+
+class TestFirls:
+
+    def test_bad_args(self):
+        # even numtaps
+        assert_raises(ValueError, firls, 10, [0.1, 0.2], [0, 0])
+        # odd bands
+        assert_raises(ValueError, firls, 11, [0.1, 0.2, 0.4], [0, 0, 0])
+        # len(bands) != len(desired)
+        assert_raises(ValueError, firls, 11, [0.1, 0.2, 0.3, 0.4], [0, 0, 0])
+        # non-monotonic bands
+        assert_raises(ValueError, firls, 11, [0.2, 0.1], [0, 0])
+        assert_raises(ValueError, firls, 11, [0.1, 0.2, 0.3, 0.3], [0] * 4)
+        assert_raises(ValueError, firls, 11, [0.3, 0.4, 0.1, 0.2], [0] * 4)
+        assert_raises(ValueError, firls, 11, [0.1, 0.3, 0.2, 0.4], [0] * 4)
+        # negative desired
+        assert_raises(ValueError, firls, 11, [0.1, 0.2], [-1, 1])
+        # len(weight) != len(pairs)
+        assert_raises(ValueError, firls, 11, [0.1, 0.2], [0, 0], weight=[1, 2])
+        # negative weight
+        assert_raises(ValueError, firls, 11, [0.1, 0.2], [0, 0], weight=[-1])
+
+    def test_firls(self):
+        N = 11  # number of taps in the filter
+        a = 0.1  # width of the transition band
+
+        # design a halfband symmetric low-pass filter
+        h = firls(11, [0, a, 0.5-a, 0.5], [1, 1, 0, 0], fs=1.0)
+
+        # make sure the filter has correct # of taps
+        assert_equal(len(h), N)
+
+        # make sure it is symmetric
+        midx = (N-1) // 2
+        assert_array_almost_equal(h[:midx], h[:-midx-1:-1])
+
+        # make sure the center tap is 0.5
+        assert_almost_equal(h[midx], 0.5)
+
+        # For halfband symmetric, odd coefficients (except the center)
+        # should be zero (really small)
+        hodd = np.hstack((h[1:midx:2], h[-midx+1::2]))
+        assert_array_almost_equal(hodd, 0)
+
+        # now check the frequency response
+        w, H = freqz(h, 1)
+        f = w/2/np.pi
+        Hmag = np.abs(H)
+
+        # check that the pass band is close to unity
+        idx = np.logical_and(f > 0, f < a)
+        assert_array_almost_equal(Hmag[idx], 1, decimal=3)
+
+        # check that the stop band is close to zero
+        idx = np.logical_and(f > 0.5-a, f < 0.5)
+        assert_array_almost_equal(Hmag[idx], 0, decimal=3)
+
+    def test_compare(self):
+        # compare to OCTAVE output
+        taps = firls(9, [0, 0.5, 0.55, 1], [1, 1, 0, 0], weight=[1, 2])
+        # >> taps = firls(8, [0 0.5 0.55 1], [1 1 0 0], [1, 2]);
+        known_taps = [-6.26930101730182e-04, -1.03354450635036e-01,
+                      -9.81576747564301e-03, 3.17271686090449e-01,
+                      5.11409425599933e-01, 3.17271686090449e-01,
+                      -9.81576747564301e-03, -1.03354450635036e-01,
+                      -6.26930101730182e-04]
+        assert_allclose(taps, known_taps)
+
+        # compare to MATLAB output
+        taps = firls(11, [0, 0.5, 0.5, 1], [1, 1, 0, 0], weight=[1, 2])
+        # >> taps = firls(10, [0 0.5 0.5 1], [1 1 0 0], [1, 2]);
+        known_taps = [
+            0.058545300496815, -0.014233383714318, -0.104688258464392,
+            0.012403323025279, 0.317930861136062, 0.488047220029700,
+            0.317930861136062, 0.012403323025279, -0.104688258464392,
+            -0.014233383714318, 0.058545300496815]
+        assert_allclose(taps, known_taps)
+
+        # With linear changes:
+        taps = firls(7, (0, 1, 2, 3, 4, 5), [1, 0, 0, 1, 1, 0], fs=20)
+        # >> taps = firls(6, [0, 0.1, 0.2, 0.3, 0.4, 0.5], [1, 0, 0, 1, 1, 0])
+        known_taps = [
+            1.156090832768218, -4.1385894727395849, 7.5288619164321826,
+            -8.5530572592947856, 7.5288619164321826, -4.1385894727395849,
+            1.156090832768218]
+        assert_allclose(taps, known_taps)
+
+    def test_rank_deficient(self):
+        # solve() runs but warns (only sometimes, so here we don't use match)
+        x = firls(21, [0, 0.1, 0.9, 1], [1, 1, 0, 0])
+        w, h = freqz(x, fs=2.)
+        assert_allclose(np.abs(h[:2]), 1., atol=1e-5)
+        assert_allclose(np.abs(h[-2:]), 0., atol=1e-6)
+        # switch to pinvh (tolerances could be higher with longer
+        # filters, but using shorter ones is faster computationally and
+        # the idea is the same)
+        x = firls(101, [0, 0.01, 0.99, 1], [1, 1, 0, 0])
+        w, h = freqz(x, fs=2.)
+        mask = w < 0.01
+        assert mask.sum() > 3
+        assert_allclose(np.abs(h[mask]), 1., atol=1e-4)
+        mask = w > 0.99
+        assert mask.sum() > 3
+        assert_allclose(np.abs(h[mask]), 0., atol=1e-4)
+
+    def test_fs_validation(self):
+        with pytest.raises(ValueError, match="Sampling.*single scalar"):
+            firls(11, .1, 1, fs=np.array([10, 20]))
+
+class TestMinimumPhase:
+
+    def test_bad_args(self):
+        # not enough taps
+        assert_raises(ValueError, minimum_phase, [1.])
+        assert_raises(ValueError, minimum_phase, [1., 1.])
+        assert_raises(ValueError, minimum_phase, np.full(10, 1j))
+        assert_raises(ValueError, minimum_phase, 'foo')
+        assert_raises(ValueError, minimum_phase, np.ones(10), n_fft=8)
+        assert_raises(ValueError, minimum_phase, np.ones(10), method='foo')
+        assert_warns(RuntimeWarning, minimum_phase, np.arange(3))
+        with pytest.raises(ValueError, match="is only supported when"):
+            minimum_phase(np.ones(3), method='hilbert', half=False)
+
+    def test_homomorphic(self):
+        # check that it can recover frequency responses of arbitrary
+        # linear-phase filters
+
+        # for some cases we can get the actual filter back
+        h = [1, -1]
+        h_new = minimum_phase(np.convolve(h, h[::-1]))
+        assert_allclose(h_new, h, rtol=0.05)
+
+        # but in general we only guarantee we get the magnitude back
+        rng = np.random.RandomState(0)
+        for n in (2, 3, 10, 11, 15, 16, 17, 20, 21, 100, 101):
+            h = rng.randn(n)
+            h_linear = np.convolve(h, h[::-1])
+            h_new = minimum_phase(h_linear)
+            assert_allclose(np.abs(fft(h_new)), np.abs(fft(h)), rtol=1e-4)
+            h_new = minimum_phase(h_linear, half=False)
+            assert len(h_linear) == len(h_new)
+            assert_allclose(np.abs(fft(h_new)), np.abs(fft(h_linear)), rtol=1e-4)
+
+    def test_hilbert(self):
+        # compare to MATLAB output of reference implementation
+
+        # f=[0 0.3 0.5 1];
+        # a=[1 1 0 0];
+        # h=remez(11,f,a);
+        h = remez(12, [0, 0.3, 0.5, 1], [1, 0], fs=2.)
+        k = [0.349585548646686, 0.373552164395447, 0.326082685363438,
+             0.077152207480935, -0.129943946349364, -0.059355880509749]
+        m = minimum_phase(h, 'hilbert')
+        assert_allclose(m, k, rtol=5e-3)
+
+        # f=[0 0.8 0.9 1];
+        # a=[0 0 1 1];
+        # h=remez(20,f,a);
+        h = remez(21, [0, 0.8, 0.9, 1], [0, 1], fs=2.)
+        k = [0.232486803906329, -0.133551833687071, 0.151871456867244,
+             -0.157957283165866, 0.151739294892963, -0.129293146705090,
+             0.100787844523204, -0.065832656741252, 0.035361328741024,
+             -0.014977068692269, -0.158416139047557]
+        m = minimum_phase(h, 'hilbert', n_fft=2**19)
+        assert_allclose(m, k, rtol=2e-3)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_ltisys.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_ltisys.py
new file mode 100644
index 0000000000000000000000000000000000000000..af69f109cd3e6e72858d69bcb338ddd61b18e3ba
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_ltisys.py
@@ -0,0 +1,1221 @@
+import warnings
+
+import numpy as np
+from numpy.testing import (assert_almost_equal, assert_equal, assert_allclose,
+                           assert_, suppress_warnings)
+from pytest import raises as assert_raises
+
+from scipy.signal import (ss2tf, tf2ss, lti,
+                          dlti, bode, freqresp, lsim, impulse, step,
+                          abcd_normalize, place_poles,
+                          TransferFunction, StateSpace, ZerosPolesGain)
+from scipy.signal._filter_design import BadCoefficients
+import scipy.linalg as linalg
+
+
+def _assert_poles_close(P1,P2, rtol=1e-8, atol=1e-8):
+    """
+    Check each pole in P1 is close to a pole in P2 with a 1e-8
+    relative tolerance or 1e-8 absolute tolerance (useful for zero poles).
+    These tolerances are very strict but the systems tested are known to
+    accept these poles so we should not be far from what is requested.
+    """
+    P2 = P2.copy()
+    for p1 in P1:
+        found = False
+        for p2_idx in range(P2.shape[0]):
+            if np.allclose([np.real(p1), np.imag(p1)],
+                           [np.real(P2[p2_idx]), np.imag(P2[p2_idx])],
+                           rtol, atol):
+                found = True
+                np.delete(P2, p2_idx)
+                break
+        if not found:
+            raise ValueError("Can't find pole " + str(p1) + " in " + str(P2))
+
+
+class TestPlacePoles:
+
+    def _check(self, A, B, P, **kwargs):
+        """
+        Perform the most common tests on the poles computed by place_poles
+        and return the Bunch object for further specific tests
+        """
+        fsf = place_poles(A, B, P, **kwargs)
+        expected, _ = np.linalg.eig(A - np.dot(B, fsf.gain_matrix))
+        _assert_poles_close(expected, fsf.requested_poles)
+        _assert_poles_close(expected, fsf.computed_poles)
+        _assert_poles_close(P,fsf.requested_poles)
+        return fsf
+
+    def test_real(self):
+        # Test real pole placement using KNV and YT0 algorithm and example 1 in
+        # section 4 of the reference publication (see place_poles docstring)
+        A = np.array([1.380, -0.2077, 6.715, -5.676, -0.5814, -4.290, 0,
+                      0.6750, 1.067, 4.273, -6.654, 5.893, 0.0480, 4.273,
+                      1.343, -2.104]).reshape(4, 4)
+        B = np.array([0, 5.679, 1.136, 1.136, 0, 0, -3.146,0]).reshape(4, 2)
+        P = np.array([-0.2, -0.5, -5.0566, -8.6659])
+
+        # Check that both KNV and YT compute correct K matrix
+        self._check(A, B, P, method='KNV0')
+        self._check(A, B, P, method='YT')
+
+        # Try to reach the specific case in _YT_real where two singular
+        # values are almost equal. This is to improve code coverage but I
+        # have no way to be sure this code is really reached
+
+        # on some architectures this can lead to a RuntimeWarning invalid
+        # value in divide (see gh-7590), so suppress it for now
+        with np.errstate(invalid='ignore'):
+            self._check(A, B, (2,2,3,3))
+
+    def test_complex(self):
+        # Test complex pole placement on a linearized car model, taken from L.
+        # Jaulin, Automatique pour la robotique, Cours et Exercices, iSTE
+        # editions p 184/185
+        A = np.array([[0, 7, 0, 0],
+                      [0, 0, 0, 7/3.],
+                      [0, 0, 0, 0],
+                      [0, 0, 0, 0]])
+        B = np.array([[0, 0],
+                      [0, 0],
+                      [1, 0],
+                      [0, 1]])
+        # Test complex poles on YT
+        P = np.array([-3, -1, -2-1j, -2+1j])
+        # on macOS arm64 this can lead to a RuntimeWarning invalid
+        # value in divide, so suppress it for now
+        with np.errstate(divide='ignore', invalid='ignore'):
+            self._check(A, B, P)
+
+        # Try to reach the specific case in _YT_complex where two singular
+        # values are almost equal. This is to improve code coverage but I
+        # have no way to be sure this code is really reached
+
+        P = [0-1e-6j,0+1e-6j,-10,10]
+        with np.errstate(divide='ignore', invalid='ignore'):
+            self._check(A, B, P, maxiter=1000)
+
+        # Try to reach the specific case in _YT_complex where the rank two
+        # update yields two null vectors. This test was found via Monte Carlo.
+
+        A = np.array(
+                    [-2148,-2902, -2267, -598, -1722, -1829, -165, -283, -2546,
+                   -167, -754, -2285, -543, -1700, -584, -2978, -925, -1300,
+                   -1583, -984, -386, -2650, -764, -897, -517, -1598, 2, -1709,
+                   -291, -338, -153, -1804, -1106, -1168, -867, -2297]
+                   ).reshape(6,6)
+
+        B = np.array(
+                    [-108, -374, -524, -1285, -1232, -161, -1204, -672, -637,
+                     -15, -483, -23, -931, -780, -1245, -1129, -1290, -1502,
+                     -952, -1374, -62, -964, -930, -939, -792, -756, -1437,
+                     -491, -1543, -686]
+                     ).reshape(6,5)
+        P = [-25.-29.j, -25.+29.j, 31.-42.j, 31.+42.j, 33.-41.j, 33.+41.j]
+        self._check(A, B, P)
+
+        # Use a lot of poles to go through all cases for update_order
+        # in _YT_loop
+
+        big_A = np.ones((11,11))-np.eye(11)
+        big_B = np.ones((11,10))-np.diag([1]*10,1)[:,1:]
+        big_A[:6,:6] = A
+        big_B[:6,:5] = B
+
+        P = [-10,-20,-30,40,50,60,70,-20-5j,-20+5j,5+3j,5-3j]
+        with np.errstate(divide='ignore', invalid='ignore'):
+            self._check(big_A, big_B, P)
+
+        #check with only complex poles and only real poles
+        P = [-10,-20,-30,-40,-50,-60,-70,-80,-90,-100]
+        self._check(big_A[:-1,:-1], big_B[:-1,:-1], P)
+        P = [-10+10j,-20+20j,-30+30j,-40+40j,-50+50j,
+             -10-10j,-20-20j,-30-30j,-40-40j,-50-50j]
+        self._check(big_A[:-1,:-1], big_B[:-1,:-1], P)
+
+        # need a 5x5 array to ensure YT handles properly when there
+        # is only one real pole and several complex
+        A = np.array([0,7,0,0,0,0,0,7/3.,0,0,0,0,0,0,0,0,
+                      0,0,0,5,0,0,0,0,9]).reshape(5,5)
+        B = np.array([0,0,0,0,1,0,0,1,2,3]).reshape(5,2)
+        P = np.array([-2, -3+1j, -3-1j, -1+1j, -1-1j])
+        with np.errstate(divide='ignore', invalid='ignore'):
+            place_poles(A, B, P)
+
+        # same test with an odd number of real poles > 1
+        # this is another specific case of YT
+        P = np.array([-2, -3, -4, -1+1j, -1-1j])
+        with np.errstate(divide='ignore', invalid='ignore'):
+            self._check(A, B, P)
+
+    def test_tricky_B(self):
+        # check we handle as we should the 1 column B matrices and
+        # n column B matrices (with n such as shape(A)=(n, n))
+        A = np.array([1.380, -0.2077, 6.715, -5.676, -0.5814, -4.290, 0,
+                      0.6750, 1.067, 4.273, -6.654, 5.893, 0.0480, 4.273,
+                      1.343, -2.104]).reshape(4, 4)
+        B = np.array([0, 5.679, 1.136, 1.136, 0, 0, -3.146, 0, 1, 2, 3, 4,
+                      5, 6, 7, 8]).reshape(4, 4)
+
+        # KNV or YT are not called here, it's a specific case with only
+        # one unique solution
+        P = np.array([-0.2, -0.5, -5.0566, -8.6659])
+        fsf = self._check(A, B, P)
+        # rtol and nb_iter should be set to np.nan as the identity can be
+        # used as transfer matrix
+        assert_equal(fsf.rtol, np.nan)
+        assert_equal(fsf.nb_iter, np.nan)
+
+        # check with complex poles too as they trigger a specific case in
+        # the specific case :-)
+        P = np.array((-2+1j,-2-1j,-3,-2))
+        fsf = self._check(A, B, P)
+        assert_equal(fsf.rtol, np.nan)
+        assert_equal(fsf.nb_iter, np.nan)
+
+        #now test with a B matrix with only one column (no optimisation)
+        B = B[:,0].reshape(4,1)
+        P = np.array((-2+1j,-2-1j,-3,-2))
+        fsf = self._check(A, B, P)
+
+        #  we can't optimize anything, check they are set to 0 as expected
+        assert_equal(fsf.rtol, 0)
+        assert_equal(fsf.nb_iter, 0)
+
+    def test_errors(self):
+        # Test input mistakes from user
+        A = np.array([0,7,0,0,0,0,0,7/3.,0,0,0,0,0,0,0,0]).reshape(4,4)
+        B = np.array([0,0,0,0,1,0,0,1]).reshape(4,2)
+
+        #should fail as the method keyword is invalid
+        assert_raises(ValueError, place_poles, A, B, (-2.1,-2.2,-2.3,-2.4),
+                      method="foo")
+
+        #should fail as poles are not 1D array
+        assert_raises(ValueError, place_poles, A, B,
+                      np.array((-2.1,-2.2,-2.3,-2.4)).reshape(4,1))
+
+        #should fail as A is not a 2D array
+        assert_raises(ValueError, place_poles, A[:,:,np.newaxis], B,
+                      (-2.1,-2.2,-2.3,-2.4))
+
+        #should fail as B is not a 2D array
+        assert_raises(ValueError, place_poles, A, B[:,:,np.newaxis],
+                      (-2.1,-2.2,-2.3,-2.4))
+
+        #should fail as there are too many poles
+        assert_raises(ValueError, place_poles, A, B, (-2.1,-2.2,-2.3,-2.4,-3))
+
+        #should fail as there are not enough poles
+        assert_raises(ValueError, place_poles, A, B, (-2.1,-2.2,-2.3))
+
+        #should fail as the rtol is greater than 1
+        assert_raises(ValueError, place_poles, A, B, (-2.1,-2.2,-2.3,-2.4),
+                      rtol=42)
+
+        #should fail as maxiter is smaller than 1
+        assert_raises(ValueError, place_poles, A, B, (-2.1,-2.2,-2.3,-2.4),
+                      maxiter=-42)
+
+        # should fail as ndim(B) is two
+        assert_raises(ValueError, place_poles, A, B, (-2,-2,-2,-2))
+
+        #unctrollable system
+        assert_raises(ValueError, place_poles, np.ones((4,4)),
+                      np.ones((4,2)), (1,2,3,4))
+
+        # Should not raise ValueError as the poles can be placed but should
+        # raise a warning as the convergence is not reached
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            fsf = place_poles(A, B, (-1,-2,-3,-4), rtol=1e-16, maxiter=42)
+            assert_(len(w) == 1)
+            assert_(issubclass(w[-1].category, UserWarning))
+            assert_("Convergence was not reached after maxiter iterations"
+                    in str(w[-1].message))
+            assert_equal(fsf.nb_iter, 42)
+
+        # should fail as a complex misses its conjugate
+        assert_raises(ValueError, place_poles, A, B, (-2+1j,-2-1j,-2+3j,-2))
+
+        # should fail as A is not square
+        assert_raises(ValueError, place_poles, A[:,:3], B, (-2,-3,-4,-5))
+
+        # should fail as B has not the same number of lines as A
+        assert_raises(ValueError, place_poles, A, B[:3,:], (-2,-3,-4,-5))
+
+        # should fail as KNV0 does not support complex poles
+        assert_raises(ValueError, place_poles, A, B,
+                      (-2+1j,-2-1j,-2+3j,-2-3j), method="KNV0")
+
+
+class TestSS2TF:
+
+    def check_matrix_shapes(self, p, q, r):
+        ss2tf(np.zeros((p, p)),
+              np.zeros((p, q)),
+              np.zeros((r, p)),
+              np.zeros((r, q)), 0)
+
+    def test_shapes(self):
+        # Each tuple holds:
+        #   number of states, number of inputs, number of outputs
+        for p, q, r in [(3, 3, 3), (1, 3, 3), (1, 1, 1)]:
+            self.check_matrix_shapes(p, q, r)
+
+    def test_basic(self):
+        # Test a round trip through tf2ss and ss2tf.
+        b = np.array([1.0, 3.0, 5.0])
+        a = np.array([1.0, 2.0, 3.0])
+
+        A, B, C, D = tf2ss(b, a)
+        assert_allclose(A, [[-2, -3], [1, 0]], rtol=1e-13)
+        assert_allclose(B, [[1], [0]], rtol=1e-13)
+        assert_allclose(C, [[1, 2]], rtol=1e-13)
+        assert_allclose(D, [[1]], rtol=1e-14)
+
+        bb, aa = ss2tf(A, B, C, D)
+        assert_allclose(bb[0], b, rtol=1e-13)
+        assert_allclose(aa, a, rtol=1e-13)
+
+    def test_zero_order_round_trip(self):
+        # See gh-5760
+        tf = (2, 1)
+        A, B, C, D = tf2ss(*tf)
+        assert_allclose(A, [[0]], rtol=1e-13)
+        assert_allclose(B, [[0]], rtol=1e-13)
+        assert_allclose(C, [[0]], rtol=1e-13)
+        assert_allclose(D, [[2]], rtol=1e-13)
+
+        num, den = ss2tf(A, B, C, D)
+        assert_allclose(num, [[2, 0]], rtol=1e-13)
+        assert_allclose(den, [1, 0], rtol=1e-13)
+
+        tf = ([[5], [2]], 1)
+        A, B, C, D = tf2ss(*tf)
+        assert_allclose(A, [[0]], rtol=1e-13)
+        assert_allclose(B, [[0]], rtol=1e-13)
+        assert_allclose(C, [[0], [0]], rtol=1e-13)
+        assert_allclose(D, [[5], [2]], rtol=1e-13)
+
+        num, den = ss2tf(A, B, C, D)
+        assert_allclose(num, [[5, 0], [2, 0]], rtol=1e-13)
+        assert_allclose(den, [1, 0], rtol=1e-13)
+
+    def test_simo_round_trip(self):
+        # See gh-5753
+        tf = ([[1, 2], [1, 1]], [1, 2])
+        A, B, C, D = tf2ss(*tf)
+        assert_allclose(A, [[-2]], rtol=1e-13)
+        assert_allclose(B, [[1]], rtol=1e-13)
+        assert_allclose(C, [[0], [-1]], rtol=1e-13)
+        assert_allclose(D, [[1], [1]], rtol=1e-13)
+
+        num, den = ss2tf(A, B, C, D)
+        assert_allclose(num, [[1, 2], [1, 1]], rtol=1e-13)
+        assert_allclose(den, [1, 2], rtol=1e-13)
+
+        tf = ([[1, 0, 1], [1, 1, 1]], [1, 1, 1])
+        A, B, C, D = tf2ss(*tf)
+        assert_allclose(A, [[-1, -1], [1, 0]], rtol=1e-13)
+        assert_allclose(B, [[1], [0]], rtol=1e-13)
+        assert_allclose(C, [[-1, 0], [0, 0]], rtol=1e-13)
+        assert_allclose(D, [[1], [1]], rtol=1e-13)
+
+        num, den = ss2tf(A, B, C, D)
+        assert_allclose(num, [[1, 0, 1], [1, 1, 1]], rtol=1e-13)
+        assert_allclose(den, [1, 1, 1], rtol=1e-13)
+
+        tf = ([[1, 2, 3], [1, 2, 3]], [1, 2, 3, 4])
+        A, B, C, D = tf2ss(*tf)
+        assert_allclose(A, [[-2, -3, -4], [1, 0, 0], [0, 1, 0]], rtol=1e-13)
+        assert_allclose(B, [[1], [0], [0]], rtol=1e-13)
+        assert_allclose(C, [[1, 2, 3], [1, 2, 3]], rtol=1e-13)
+        assert_allclose(D, [[0], [0]], rtol=1e-13)
+
+        num, den = ss2tf(A, B, C, D)
+        assert_allclose(num, [[0, 1, 2, 3], [0, 1, 2, 3]], rtol=1e-13)
+        assert_allclose(den, [1, 2, 3, 4], rtol=1e-13)
+
+        tf = (np.array([1, [2, 3]], dtype=object), [1, 6])
+        A, B, C, D = tf2ss(*tf)
+        assert_allclose(A, [[-6]], rtol=1e-31)
+        assert_allclose(B, [[1]], rtol=1e-31)
+        assert_allclose(C, [[1], [-9]], rtol=1e-31)
+        assert_allclose(D, [[0], [2]], rtol=1e-31)
+
+        num, den = ss2tf(A, B, C, D)
+        assert_allclose(num, [[0, 1], [2, 3]], rtol=1e-13)
+        assert_allclose(den, [1, 6], rtol=1e-13)
+
+        tf = (np.array([[1, -3], [1, 2, 3]], dtype=object), [1, 6, 5])
+        A, B, C, D = tf2ss(*tf)
+        assert_allclose(A, [[-6, -5], [1, 0]], rtol=1e-13)
+        assert_allclose(B, [[1], [0]], rtol=1e-13)
+        assert_allclose(C, [[1, -3], [-4, -2]], rtol=1e-13)
+        assert_allclose(D, [[0], [1]], rtol=1e-13)
+
+        num, den = ss2tf(A, B, C, D)
+        assert_allclose(num, [[0, 1, -3], [1, 2, 3]], rtol=1e-13)
+        assert_allclose(den, [1, 6, 5], rtol=1e-13)
+
+    def test_all_int_arrays(self):
+        A = [[0, 1, 0], [0, 0, 1], [-3, -4, -2]]
+        B = [[0], [0], [1]]
+        C = [[5, 1, 0]]
+        D = [[0]]
+        num, den = ss2tf(A, B, C, D)
+        assert_allclose(num, [[0.0, 0.0, 1.0, 5.0]], rtol=1e-13, atol=1e-14)
+        assert_allclose(den, [1.0, 2.0, 4.0, 3.0], rtol=1e-13)
+
+    def test_multioutput(self):
+        # Regression test for gh-2669.
+
+        # 4 states
+        A = np.array([[-1.0, 0.0, 1.0, 0.0],
+                      [-1.0, 0.0, 2.0, 0.0],
+                      [-4.0, 0.0, 3.0, 0.0],
+                      [-8.0, 8.0, 0.0, 4.0]])
+
+        # 1 input
+        B = np.array([[0.3],
+                      [0.0],
+                      [7.0],
+                      [0.0]])
+
+        # 3 outputs
+        C = np.array([[0.0, 1.0, 0.0, 0.0],
+                      [0.0, 0.0, 0.0, 1.0],
+                      [8.0, 8.0, 0.0, 0.0]])
+
+        D = np.array([[0.0],
+                      [0.0],
+                      [1.0]])
+
+        # Get the transfer functions for all the outputs in one call.
+        b_all, a = ss2tf(A, B, C, D)
+
+        # Get the transfer functions for each output separately.
+        b0, a0 = ss2tf(A, B, C[0], D[0])
+        b1, a1 = ss2tf(A, B, C[1], D[1])
+        b2, a2 = ss2tf(A, B, C[2], D[2])
+
+        # Check that we got the same results.
+        assert_allclose(a0, a, rtol=1e-13)
+        assert_allclose(a1, a, rtol=1e-13)
+        assert_allclose(a2, a, rtol=1e-13)
+        assert_allclose(b_all, np.vstack((b0, b1, b2)), rtol=1e-13, atol=1e-14)
+
+
+class TestLsim:
+    digits_accuracy = 7
+
+    def lti_nowarn(self, *args):
+        with suppress_warnings() as sup:
+            sup.filter(BadCoefficients)
+            system = lti(*args)
+        return system
+
+    def test_first_order(self):
+        # y' = -y
+        # exact solution is y(t) = exp(-t)
+        system = self.lti_nowarn(-1.,1.,1.,0.)
+        t = np.linspace(0,5)
+        u = np.zeros_like(t)
+        tout, y, x = lsim(system, u, t, X0=[1.0])
+        expected_x = np.exp(-tout)
+        assert_almost_equal(x, expected_x)
+        assert_almost_equal(y, expected_x)
+
+    def test_second_order(self):
+        t = np.linspace(0, 10, 1001)
+        u = np.zeros_like(t)
+        # Second order system with a repeated root: x''(t) + 2*x(t) + x(t) = 0.
+        # With initial conditions x(0)=1.0 and x'(t)=0.0, the exact solution
+        # is (1-t)*exp(-t).
+        system = self.lti_nowarn([1.0], [1.0, 2.0, 1.0])
+        tout, y, x = lsim(system, u, t, X0=[1.0, 0.0])
+        expected_x = (1.0 - tout) * np.exp(-tout)
+        assert_almost_equal(x[:, 0], expected_x)
+
+    def test_integrator(self):
+        # integrator: y' = u
+        system = self.lti_nowarn(0., 1., 1., 0.)
+        t = np.linspace(0,5)
+        u = t
+        tout, y, x = lsim(system, u, t)
+        expected_x = 0.5 * tout**2
+        assert_almost_equal(x, expected_x, decimal=self.digits_accuracy)
+        assert_almost_equal(y, expected_x, decimal=self.digits_accuracy)
+
+    def test_two_states(self):
+        # A system with two state variables, two inputs, and one output.
+        A = np.array([[-1.0, 0.0], [0.0, -2.0]])
+        B = np.array([[1.0, 0.0], [0.0, 1.0]])
+        C = np.array([1.0, 0.0])
+        D = np.zeros((1, 2))
+
+        system = self.lti_nowarn(A, B, C, D)
+
+        t = np.linspace(0, 10.0, 21)
+        u = np.zeros((len(t), 2))
+        tout, y, x = lsim(system, U=u, T=t, X0=[1.0, 1.0])
+        expected_y = np.exp(-tout)
+        expected_x0 = np.exp(-tout)
+        expected_x1 = np.exp(-2.0 * tout)
+        assert_almost_equal(y, expected_y)
+        assert_almost_equal(x[:, 0], expected_x0)
+        assert_almost_equal(x[:, 1], expected_x1)
+
+    def test_double_integrator(self):
+        # double integrator: y'' = 2u
+        A = np.array([[0., 1.], [0., 0.]])
+        B = np.array([[0.], [1.]])
+        C = np.array([[2., 0.]])
+        system = self.lti_nowarn(A, B, C, 0.)
+        t = np.linspace(0,5)
+        u = np.ones_like(t)
+        tout, y, x = lsim(system, u, t)
+        expected_x = np.transpose(np.array([0.5 * tout**2, tout]))
+        expected_y = tout**2
+        assert_almost_equal(x, expected_x, decimal=self.digits_accuracy)
+        assert_almost_equal(y, expected_y, decimal=self.digits_accuracy)
+
+    def test_jordan_block(self):
+        # Non-diagonalizable A matrix
+        #   x1' + x1 = x2
+        #   x2' + x2 = u
+        #   y = x1
+        # Exact solution with u = 0 is y(t) = t exp(-t)
+        A = np.array([[-1., 1.], [0., -1.]])
+        B = np.array([[0.], [1.]])
+        C = np.array([[1., 0.]])
+        system = self.lti_nowarn(A, B, C, 0.)
+        t = np.linspace(0,5)
+        u = np.zeros_like(t)
+        tout, y, x = lsim(system, u, t, X0=[0.0, 1.0])
+        expected_y = tout * np.exp(-tout)
+        assert_almost_equal(y, expected_y)
+
+    def test_miso(self):
+        # A system with two state variables, two inputs, and one output.
+        A = np.array([[-1.0, 0.0], [0.0, -2.0]])
+        B = np.array([[1.0, 0.0], [0.0, 1.0]])
+        C = np.array([1.0, 0.0])
+        D = np.zeros((1,2))
+        system = self.lti_nowarn(A, B, C, D)
+
+        t = np.linspace(0, 5.0, 101)
+        u = np.zeros((len(t), 2))
+        tout, y, x = lsim(system, u, t, X0=[1.0, 1.0])
+        expected_y = np.exp(-tout)
+        expected_x0 = np.exp(-tout)
+        expected_x1 = np.exp(-2.0*tout)
+        assert_almost_equal(y, expected_y)
+        assert_almost_equal(x[:,0], expected_x0)
+        assert_almost_equal(x[:,1], expected_x1)
+
+    def test_nonzero_initial_time(self):
+        system = self.lti_nowarn(-1.,1.,1.,0.)
+        t = np.linspace(1,2)
+        u = np.zeros_like(t)
+        tout, y, x = lsim(system, u, t, X0=[1.0])
+        expected_y = np.exp(-tout)
+        assert_almost_equal(y, expected_y)
+
+    def test_nonequal_timesteps(self):
+        t = np.array([0.0, 1.0, 1.0, 3.0])
+        u = np.array([0.0, 0.0, 1.0, 1.0])
+        # Simple integrator: x'(t) = u(t)
+        system = ([1.0], [1.0, 0.0])
+        with assert_raises(ValueError,
+                           match="Time steps are not equally spaced."):
+            tout, y, x = lsim(system, u, t, X0=[1.0])
+
+
+class TestImpulse:
+    def test_first_order(self):
+        # First order system: x'(t) + x(t) = u(t)
+        # Exact impulse response is x(t) = exp(-t).
+        system = ([1.0], [1.0,1.0])
+        tout, y = impulse(system)
+        expected_y = np.exp(-tout)
+        assert_almost_equal(y, expected_y)
+
+    def test_first_order_fixed_time(self):
+        # Specify the desired time values for the output.
+
+        # First order system: x'(t) + x(t) = u(t)
+        # Exact impulse response is x(t) = exp(-t).
+        system = ([1.0], [1.0,1.0])
+        n = 21
+        t = np.linspace(0, 2.0, n)
+        tout, y = impulse(system, T=t)
+        assert_equal(tout.shape, (n,))
+        assert_almost_equal(tout, t)
+        expected_y = np.exp(-t)
+        assert_almost_equal(y, expected_y)
+
+    def test_first_order_initial(self):
+        # Specify an initial condition as a scalar.
+
+        # First order system: x'(t) + x(t) = u(t), x(0)=3.0
+        # Exact impulse response is x(t) = 4*exp(-t).
+        system = ([1.0], [1.0,1.0])
+        tout, y = impulse(system, X0=3.0)
+        expected_y = 4.0 * np.exp(-tout)
+        assert_almost_equal(y, expected_y)
+
+    def test_first_order_initial_list(self):
+        # Specify an initial condition as a list.
+
+        # First order system: x'(t) + x(t) = u(t), x(0)=3.0
+        # Exact impulse response is x(t) = 4*exp(-t).
+        system = ([1.0], [1.0,1.0])
+        tout, y = impulse(system, X0=[3.0])
+        expected_y = 4.0 * np.exp(-tout)
+        assert_almost_equal(y, expected_y)
+
+    def test_integrator(self):
+        # Simple integrator: x'(t) = u(t)
+        system = ([1.0], [1.0,0.0])
+        tout, y = impulse(system)
+        expected_y = np.ones_like(tout)
+        assert_almost_equal(y, expected_y)
+
+    def test_second_order(self):
+        # Second order system with a repeated root:
+        #     x''(t) + 2*x(t) + x(t) = u(t)
+        # The exact impulse response is t*exp(-t).
+        system = ([1.0], [1.0, 2.0, 1.0])
+        tout, y = impulse(system)
+        expected_y = tout * np.exp(-tout)
+        assert_almost_equal(y, expected_y)
+
+    def test_array_like(self):
+        # Test that function can accept sequences, scalars.
+        system = ([1.0], [1.0, 2.0, 1.0])
+        # TODO: add meaningful test where X0 is a list
+        tout, y = impulse(system, X0=[3], T=[5, 6])
+        tout, y = impulse(system, X0=[3], T=[5])
+
+    def test_array_like2(self):
+        system = ([1.0], [1.0, 2.0, 1.0])
+        tout, y = impulse(system, X0=3, T=5)
+
+
+class TestStep:
+    def test_first_order(self):
+        # First order system: x'(t) + x(t) = u(t)
+        # Exact step response is x(t) = 1 - exp(-t).
+        system = ([1.0], [1.0,1.0])
+        tout, y = step(system)
+        expected_y = 1.0 - np.exp(-tout)
+        assert_almost_equal(y, expected_y)
+
+    def test_first_order_fixed_time(self):
+        # Specify the desired time values for the output.
+
+        # First order system: x'(t) + x(t) = u(t)
+        # Exact step response is x(t) = 1 - exp(-t).
+        system = ([1.0], [1.0,1.0])
+        n = 21
+        t = np.linspace(0, 2.0, n)
+        tout, y = step(system, T=t)
+        assert_equal(tout.shape, (n,))
+        assert_almost_equal(tout, t)
+        expected_y = 1 - np.exp(-t)
+        assert_almost_equal(y, expected_y)
+
+    def test_first_order_initial(self):
+        # Specify an initial condition as a scalar.
+
+        # First order system: x'(t) + x(t) = u(t), x(0)=3.0
+        # Exact step response is x(t) = 1 + 2*exp(-t).
+        system = ([1.0], [1.0,1.0])
+        tout, y = step(system, X0=3.0)
+        expected_y = 1 + 2.0*np.exp(-tout)
+        assert_almost_equal(y, expected_y)
+
+    def test_first_order_initial_list(self):
+        # Specify an initial condition as a list.
+
+        # First order system: x'(t) + x(t) = u(t), x(0)=3.0
+        # Exact step response is x(t) = 1 + 2*exp(-t).
+        system = ([1.0], [1.0,1.0])
+        tout, y = step(system, X0=[3.0])
+        expected_y = 1 + 2.0*np.exp(-tout)
+        assert_almost_equal(y, expected_y)
+
+    def test_integrator(self):
+        # Simple integrator: x'(t) = u(t)
+        # Exact step response is x(t) = t.
+        system = ([1.0],[1.0,0.0])
+        tout, y = step(system)
+        expected_y = tout
+        assert_almost_equal(y, expected_y)
+
+    def test_second_order(self):
+        # Second order system with a repeated root:
+        #     x''(t) + 2*x(t) + x(t) = u(t)
+        # The exact step response is 1 - (1 + t)*exp(-t).
+        system = ([1.0], [1.0, 2.0, 1.0])
+        tout, y = step(system)
+        expected_y = 1 - (1 + tout) * np.exp(-tout)
+        assert_almost_equal(y, expected_y)
+
+    def test_array_like(self):
+        # Test that function can accept sequences, scalars.
+        system = ([1.0], [1.0, 2.0, 1.0])
+        # TODO: add meaningful test where X0 is a list
+        tout, y = step(system, T=[5, 6])
+
+    def test_complex_input(self):
+        # Test that complex input doesn't raise an error.
+        # `step` doesn't seem to have been designed for complex input, but this
+        # works and may be used, so add regression test.  See gh-2654.
+        step(([], [-1], 1+0j))
+
+
+class TestLti:
+    def test_lti_instantiation(self):
+        # Test that lti can be instantiated with sequences, scalars.
+        # See PR-225.
+
+        # TransferFunction
+        s = lti([1], [-1])
+        assert_(isinstance(s, TransferFunction))
+        assert_(isinstance(s, lti))
+        assert_(not isinstance(s, dlti))
+        assert_(s.dt is None)
+
+        # ZerosPolesGain
+        s = lti(np.array([]), np.array([-1]), 1)
+        assert_(isinstance(s, ZerosPolesGain))
+        assert_(isinstance(s, lti))
+        assert_(not isinstance(s, dlti))
+        assert_(s.dt is None)
+
+        # StateSpace
+        s = lti([], [-1], 1)
+        s = lti([1], [-1], 1, 3)
+        assert_(isinstance(s, StateSpace))
+        assert_(isinstance(s, lti))
+        assert_(not isinstance(s, dlti))
+        assert_(s.dt is None)
+
+
+class TestStateSpace:
+    def test_initialization(self):
+        # Check that all initializations work
+        StateSpace(1, 1, 1, 1)
+        StateSpace([1], [2], [3], [4])
+        StateSpace(np.array([[1, 2], [3, 4]]), np.array([[1], [2]]),
+                   np.array([[1, 0]]), np.array([[0]]))
+
+    def test_conversion(self):
+        # Check the conversion functions
+        s = StateSpace(1, 2, 3, 4)
+        assert_(isinstance(s.to_ss(), StateSpace))
+        assert_(isinstance(s.to_tf(), TransferFunction))
+        assert_(isinstance(s.to_zpk(), ZerosPolesGain))
+
+        # Make sure copies work
+        assert_(StateSpace(s) is not s)
+        assert_(s.to_ss() is not s)
+
+    def test_properties(self):
+        # Test setters/getters for cross class properties.
+        # This implicitly tests to_tf() and to_zpk()
+
+        # Getters
+        s = StateSpace(1, 1, 1, 1)
+        assert_equal(s.poles, [1])
+        assert_equal(s.zeros, [0])
+        assert_(s.dt is None)
+
+    def test_operators(self):
+        # Test +/-/* operators on systems
+
+        class BadType:
+            pass
+
+        s1 = StateSpace(np.array([[-0.5, 0.7], [0.3, -0.8]]),
+                        np.array([[1], [0]]),
+                        np.array([[1, 0]]),
+                        np.array([[0]]),
+                        )
+
+        s2 = StateSpace(np.array([[-0.2, -0.1], [0.4, -0.1]]),
+                        np.array([[1], [0]]),
+                        np.array([[1, 0]]),
+                        np.array([[0]])
+                        )
+
+        s_discrete = s1.to_discrete(0.1)
+        s2_discrete = s2.to_discrete(0.2)
+        s3_discrete = s2.to_discrete(0.1)
+
+        # Impulse response
+        t = np.linspace(0, 1, 100)
+        u = np.zeros_like(t)
+        u[0] = 1
+
+        # Test multiplication
+        for typ in (int, float, complex, np.float32, np.complex128, np.array):
+            assert_allclose(lsim(typ(2) * s1, U=u, T=t)[1],
+                            typ(2) * lsim(s1, U=u, T=t)[1])
+
+            assert_allclose(lsim(s1 * typ(2), U=u, T=t)[1],
+                            lsim(s1, U=u, T=t)[1] * typ(2))
+
+            assert_allclose(lsim(s1 / typ(2), U=u, T=t)[1],
+                            lsim(s1, U=u, T=t)[1] / typ(2))
+
+            with assert_raises(TypeError):
+                typ(2) / s1
+
+        assert_allclose(lsim(s1 * 2, U=u, T=t)[1],
+                        lsim(s1, U=2 * u, T=t)[1])
+
+        assert_allclose(lsim(s1 * s2, U=u, T=t)[1],
+                        lsim(s1, U=lsim(s2, U=u, T=t)[1], T=t)[1],
+                        atol=1e-5)
+
+        with assert_raises(TypeError):
+            s1 / s1
+
+        with assert_raises(TypeError):
+            s1 * s_discrete
+
+        with assert_raises(TypeError):
+            # Check different discretization constants
+            s_discrete * s2_discrete
+
+        with assert_raises(TypeError):
+            s1 * BadType()
+
+        with assert_raises(TypeError):
+            BadType() * s1
+
+        with assert_raises(TypeError):
+            s1 / BadType()
+
+        with assert_raises(TypeError):
+            BadType() / s1
+
+        # Test addition
+        assert_allclose(lsim(s1 + 2, U=u, T=t)[1],
+                        2 * u + lsim(s1, U=u, T=t)[1])
+
+        # Check for dimension mismatch
+        with assert_raises(ValueError):
+            s1 + np.array([1, 2])
+
+        with assert_raises(ValueError):
+            np.array([1, 2]) + s1
+
+        with assert_raises(TypeError):
+            s1 + s_discrete
+
+        with assert_raises(ValueError):
+            s1 / np.array([[1, 2], [3, 4]])
+
+        with assert_raises(TypeError):
+            # Check different discretization constants
+            s_discrete + s2_discrete
+
+        with assert_raises(TypeError):
+            s1 + BadType()
+
+        with assert_raises(TypeError):
+            BadType() + s1
+
+        assert_allclose(lsim(s1 + s2, U=u, T=t)[1],
+                        lsim(s1, U=u, T=t)[1] + lsim(s2, U=u, T=t)[1])
+
+        # Test subtraction
+        assert_allclose(lsim(s1 - 2, U=u, T=t)[1],
+                        -2 * u + lsim(s1, U=u, T=t)[1])
+
+        assert_allclose(lsim(2 - s1, U=u, T=t)[1],
+                        2 * u + lsim(-s1, U=u, T=t)[1])
+
+        assert_allclose(lsim(s1 - s2, U=u, T=t)[1],
+                        lsim(s1, U=u, T=t)[1] - lsim(s2, U=u, T=t)[1])
+
+        with assert_raises(TypeError):
+            s1 - BadType()
+
+        with assert_raises(TypeError):
+            BadType() - s1
+
+        s = s_discrete + s3_discrete
+        assert_(s.dt == 0.1)
+
+        s = s_discrete * s3_discrete
+        assert_(s.dt == 0.1)
+
+        s = 3 * s_discrete
+        assert_(s.dt == 0.1)
+
+        s = -s_discrete
+        assert_(s.dt == 0.1)
+
+class TestTransferFunction:
+    def test_initialization(self):
+        # Check that all initializations work
+        TransferFunction(1, 1)
+        TransferFunction([1], [2])
+        TransferFunction(np.array([1]), np.array([2]))
+
+    def test_conversion(self):
+        # Check the conversion functions
+        s = TransferFunction([1, 0], [1, -1])
+        assert_(isinstance(s.to_ss(), StateSpace))
+        assert_(isinstance(s.to_tf(), TransferFunction))
+        assert_(isinstance(s.to_zpk(), ZerosPolesGain))
+
+        # Make sure copies work
+        assert_(TransferFunction(s) is not s)
+        assert_(s.to_tf() is not s)
+
+    def test_properties(self):
+        # Test setters/getters for cross class properties.
+        # This implicitly tests to_ss() and to_zpk()
+
+        # Getters
+        s = TransferFunction([1, 0], [1, -1])
+        assert_equal(s.poles, [1])
+        assert_equal(s.zeros, [0])
+
+
+class TestZerosPolesGain:
+    def test_initialization(self):
+        # Check that all initializations work
+        ZerosPolesGain(1, 1, 1)
+        ZerosPolesGain([1], [2], 1)
+        ZerosPolesGain(np.array([1]), np.array([2]), 1)
+
+    def test_conversion(self):
+        #Check the conversion functions
+        s = ZerosPolesGain(1, 2, 3)
+        assert_(isinstance(s.to_ss(), StateSpace))
+        assert_(isinstance(s.to_tf(), TransferFunction))
+        assert_(isinstance(s.to_zpk(), ZerosPolesGain))
+
+        # Make sure copies work
+        assert_(ZerosPolesGain(s) is not s)
+        assert_(s.to_zpk() is not s)
+
+
+class Test_abcd_normalize:
+    def setup_method(self):
+        self.A = np.array([[1.0, 2.0], [3.0, 4.0]])
+        self.B = np.array([[-1.0], [5.0]])
+        self.C = np.array([[4.0, 5.0]])
+        self.D = np.array([[2.5]])
+
+    def test_no_matrix_fails(self):
+        assert_raises(ValueError, abcd_normalize)
+
+    def test_A_nosquare_fails(self):
+        assert_raises(ValueError, abcd_normalize, [1, -1],
+                      self.B, self.C, self.D)
+
+    def test_AB_mismatch_fails(self):
+        assert_raises(ValueError, abcd_normalize, self.A, [-1, 5],
+                      self.C, self.D)
+
+    def test_AC_mismatch_fails(self):
+        assert_raises(ValueError, abcd_normalize, self.A, self.B,
+                      [[4.0], [5.0]], self.D)
+
+    def test_CD_mismatch_fails(self):
+        assert_raises(ValueError, abcd_normalize, self.A, self.B,
+                      self.C, [2.5, 0])
+
+    def test_BD_mismatch_fails(self):
+        assert_raises(ValueError, abcd_normalize, self.A, [-1, 5],
+                      self.C, self.D)
+
+    def test_normalized_matrices_unchanged(self):
+        A, B, C, D = abcd_normalize(self.A, self.B, self.C, self.D)
+        assert_equal(A, self.A)
+        assert_equal(B, self.B)
+        assert_equal(C, self.C)
+        assert_equal(D, self.D)
+
+    def test_shapes(self):
+        A, B, C, D = abcd_normalize(self.A, self.B, [1, 0], 0)
+        assert_equal(A.shape[0], A.shape[1])
+        assert_equal(A.shape[0], B.shape[0])
+        assert_equal(A.shape[0], C.shape[1])
+        assert_equal(C.shape[0], D.shape[0])
+        assert_equal(B.shape[1], D.shape[1])
+
+    def test_zero_dimension_is_not_none1(self):
+        B_ = np.zeros((2, 0))
+        D_ = np.zeros((0, 0))
+        A, B, C, D = abcd_normalize(A=self.A, B=B_, D=D_)
+        assert_equal(A, self.A)
+        assert_equal(B, B_)
+        assert_equal(D, D_)
+        assert_equal(C.shape[0], D_.shape[0])
+        assert_equal(C.shape[1], self.A.shape[0])
+
+    def test_zero_dimension_is_not_none2(self):
+        B_ = np.zeros((2, 0))
+        C_ = np.zeros((0, 2))
+        A, B, C, D = abcd_normalize(A=self.A, B=B_, C=C_)
+        assert_equal(A, self.A)
+        assert_equal(B, B_)
+        assert_equal(C, C_)
+        assert_equal(D.shape[0], C_.shape[0])
+        assert_equal(D.shape[1], B_.shape[1])
+
+    def test_missing_A(self):
+        A, B, C, D = abcd_normalize(B=self.B, C=self.C, D=self.D)
+        assert_equal(A.shape[0], A.shape[1])
+        assert_equal(A.shape[0], B.shape[0])
+        assert_equal(A.shape, (self.B.shape[0], self.B.shape[0]))
+
+    def test_missing_B(self):
+        A, B, C, D = abcd_normalize(A=self.A, C=self.C, D=self.D)
+        assert_equal(B.shape[0], A.shape[0])
+        assert_equal(B.shape[1], D.shape[1])
+        assert_equal(B.shape, (self.A.shape[0], self.D.shape[1]))
+
+    def test_missing_C(self):
+        A, B, C, D = abcd_normalize(A=self.A, B=self.B, D=self.D)
+        assert_equal(C.shape[0], D.shape[0])
+        assert_equal(C.shape[1], A.shape[0])
+        assert_equal(C.shape, (self.D.shape[0], self.A.shape[0]))
+
+    def test_missing_D(self):
+        A, B, C, D = abcd_normalize(A=self.A, B=self.B, C=self.C)
+        assert_equal(D.shape[0], C.shape[0])
+        assert_equal(D.shape[1], B.shape[1])
+        assert_equal(D.shape, (self.C.shape[0], self.B.shape[1]))
+
+    def test_missing_AB(self):
+        A, B, C, D = abcd_normalize(C=self.C, D=self.D)
+        assert_equal(A.shape[0], A.shape[1])
+        assert_equal(A.shape[0], B.shape[0])
+        assert_equal(B.shape[1], D.shape[1])
+        assert_equal(A.shape, (self.C.shape[1], self.C.shape[1]))
+        assert_equal(B.shape, (self.C.shape[1], self.D.shape[1]))
+
+    def test_missing_AC(self):
+        A, B, C, D = abcd_normalize(B=self.B, D=self.D)
+        assert_equal(A.shape[0], A.shape[1])
+        assert_equal(A.shape[0], B.shape[0])
+        assert_equal(C.shape[0], D.shape[0])
+        assert_equal(C.shape[1], A.shape[0])
+        assert_equal(A.shape, (self.B.shape[0], self.B.shape[0]))
+        assert_equal(C.shape, (self.D.shape[0], self.B.shape[0]))
+
+    def test_missing_AD(self):
+        A, B, C, D = abcd_normalize(B=self.B, C=self.C)
+        assert_equal(A.shape[0], A.shape[1])
+        assert_equal(A.shape[0], B.shape[0])
+        assert_equal(D.shape[0], C.shape[0])
+        assert_equal(D.shape[1], B.shape[1])
+        assert_equal(A.shape, (self.B.shape[0], self.B.shape[0]))
+        assert_equal(D.shape, (self.C.shape[0], self.B.shape[1]))
+
+    def test_missing_BC(self):
+        A, B, C, D = abcd_normalize(A=self.A, D=self.D)
+        assert_equal(B.shape[0], A.shape[0])
+        assert_equal(B.shape[1], D.shape[1])
+        assert_equal(C.shape[0], D.shape[0])
+        assert_equal(C.shape[1], A.shape[0])
+        assert_equal(B.shape, (self.A.shape[0], self.D.shape[1]))
+        assert_equal(C.shape, (self.D.shape[0], self.A.shape[0]))
+
+    def test_missing_ABC_fails(self):
+        assert_raises(ValueError, abcd_normalize, D=self.D)
+
+    def test_missing_BD_fails(self):
+        assert_raises(ValueError, abcd_normalize, A=self.A, C=self.C)
+
+    def test_missing_CD_fails(self):
+        assert_raises(ValueError, abcd_normalize, A=self.A, B=self.B)
+
+
+class Test_bode:
+
+    def test_01(self):
+        # Test bode() magnitude calculation (manual sanity check).
+        # 1st order low-pass filter: H(s) = 1 / (s + 1),
+        # cutoff: 1 rad/s, slope: -20 dB/decade
+        #   H(s=0.1) ~= 0 dB
+        #   H(s=1) ~= -3 dB
+        #   H(s=10) ~= -20 dB
+        #   H(s=100) ~= -40 dB
+        system = lti([1], [1, 1])
+        w = [0.1, 1, 10, 100]
+        w, mag, phase = bode(system, w=w)
+        expected_mag = [0, -3, -20, -40]
+        assert_almost_equal(mag, expected_mag, decimal=1)
+
+    def test_02(self):
+        # Test bode() phase calculation (manual sanity check).
+        # 1st order low-pass filter: H(s) = 1 / (s + 1),
+        #   angle(H(s=0.1)) ~= -5.7 deg
+        #   angle(H(s=1)) ~= -45 deg
+        #   angle(H(s=10)) ~= -84.3 deg
+        system = lti([1], [1, 1])
+        w = [0.1, 1, 10]
+        w, mag, phase = bode(system, w=w)
+        expected_phase = [-5.7, -45, -84.3]
+        assert_almost_equal(phase, expected_phase, decimal=1)
+
+    def test_03(self):
+        # Test bode() magnitude calculation.
+        # 1st order low-pass filter: H(s) = 1 / (s + 1)
+        system = lti([1], [1, 1])
+        w = [0.1, 1, 10, 100]
+        w, mag, phase = bode(system, w=w)
+        jw = w * 1j
+        y = np.polyval(system.num, jw) / np.polyval(system.den, jw)
+        expected_mag = 20.0 * np.log10(abs(y))
+        assert_almost_equal(mag, expected_mag)
+
+    def test_04(self):
+        # Test bode() phase calculation.
+        # 1st order low-pass filter: H(s) = 1 / (s + 1)
+        system = lti([1], [1, 1])
+        w = [0.1, 1, 10, 100]
+        w, mag, phase = bode(system, w=w)
+        jw = w * 1j
+        y = np.polyval(system.num, jw) / np.polyval(system.den, jw)
+        expected_phase = np.arctan2(y.imag, y.real) * 180.0 / np.pi
+        assert_almost_equal(phase, expected_phase)
+
+    def test_05(self):
+        # Test that bode() finds a reasonable frequency range.
+        # 1st order low-pass filter: H(s) = 1 / (s + 1)
+        system = lti([1], [1, 1])
+        n = 10
+        # Expected range is from 0.01 to 10.
+        expected_w = np.logspace(-2, 1, n)
+        w, mag, phase = bode(system, n=n)
+        assert_almost_equal(w, expected_w)
+
+    def test_06(self):
+        # Test that bode() doesn't fail on a system with a pole at 0.
+        # integrator, pole at zero: H(s) = 1 / s
+        system = lti([1], [1, 0])
+        w, mag, phase = bode(system, n=2)
+        assert_equal(w[0], 0.01)  # a fail would give not-a-number
+
+    def test_07(self):
+        # bode() should not fail on a system with pure imaginary poles.
+        # The test passes if bode doesn't raise an exception.
+        system = lti([1], [1, 0, 100])
+        w, mag, phase = bode(system, n=2)
+
+    def test_08(self):
+        # Test that bode() return continuous phase, issues/2331.
+        system = lti([], [-10, -30, -40, -60, -70], 1)
+        w, mag, phase = system.bode(w=np.logspace(-3, 40, 100))
+        assert_almost_equal(min(phase), -450, decimal=15)
+
+    def test_from_state_space(self):
+        # Ensure that bode works with a system that was created from the
+        # state space representation matrices A, B, C, D.  In this case,
+        # system.num will be a 2-D array with shape (1, n+1), where (n,n)
+        # is the shape of A.
+        # A Butterworth lowpass filter is used, so we know the exact
+        # frequency response.
+        a = np.array([1.0, 2.0, 2.0, 1.0])
+        A = linalg.companion(a).T
+        B = np.array([[0.0], [0.0], [1.0]])
+        C = np.array([[1.0, 0.0, 0.0]])
+        D = np.array([[0.0]])
+        with suppress_warnings() as sup:
+            sup.filter(BadCoefficients)
+            system = lti(A, B, C, D)
+            w, mag, phase = bode(system, n=100)
+
+        expected_magnitude = 20 * np.log10(np.sqrt(1.0 / (1.0 + w**6)))
+        assert_almost_equal(mag, expected_magnitude)
+
+
+class Test_freqresp:
+
+    def test_output_manual(self):
+        # Test freqresp() output calculation (manual sanity check).
+        # 1st order low-pass filter: H(s) = 1 / (s + 1),
+        #   re(H(s=0.1)) ~= 0.99
+        #   re(H(s=1)) ~= 0.5
+        #   re(H(s=10)) ~= 0.0099
+        system = lti([1], [1, 1])
+        w = [0.1, 1, 10]
+        w, H = freqresp(system, w=w)
+        expected_re = [0.99, 0.5, 0.0099]
+        expected_im = [-0.099, -0.5, -0.099]
+        assert_almost_equal(H.real, expected_re, decimal=1)
+        assert_almost_equal(H.imag, expected_im, decimal=1)
+
+    def test_output(self):
+        # Test freqresp() output calculation.
+        # 1st order low-pass filter: H(s) = 1 / (s + 1)
+        system = lti([1], [1, 1])
+        w = [0.1, 1, 10, 100]
+        w, H = freqresp(system, w=w)
+        s = w * 1j
+        expected = np.polyval(system.num, s) / np.polyval(system.den, s)
+        assert_almost_equal(H.real, expected.real)
+        assert_almost_equal(H.imag, expected.imag)
+
+    def test_freq_range(self):
+        # Test that freqresp() finds a reasonable frequency range.
+        # 1st order low-pass filter: H(s) = 1 / (s + 1)
+        # Expected range is from 0.01 to 10.
+        system = lti([1], [1, 1])
+        n = 10
+        expected_w = np.logspace(-2, 1, n)
+        w, H = freqresp(system, n=n)
+        assert_almost_equal(w, expected_w)
+
+    def test_pole_zero(self):
+        # Test that freqresp() doesn't fail on a system with a pole at 0.
+        # integrator, pole at zero: H(s) = 1 / s
+        system = lti([1], [1, 0])
+        w, H = freqresp(system, n=2)
+        assert_equal(w[0], 0.01)  # a fail would give not-a-number
+
+    def test_from_state_space(self):
+        # Ensure that freqresp works with a system that was created from the
+        # state space representation matrices A, B, C, D.  In this case,
+        # system.num will be a 2-D array with shape (1, n+1), where (n,n) is
+        # the shape of A.
+        # A Butterworth lowpass filter is used, so we know the exact
+        # frequency response.
+        a = np.array([1.0, 2.0, 2.0, 1.0])
+        A = linalg.companion(a).T
+        B = np.array([[0.0],[0.0],[1.0]])
+        C = np.array([[1.0, 0.0, 0.0]])
+        D = np.array([[0.0]])
+        with suppress_warnings() as sup:
+            sup.filter(BadCoefficients)
+            system = lti(A, B, C, D)
+            w, H = freqresp(system, n=100)
+        s = w * 1j
+        expected = (1.0 / (1.0 + 2*s + 2*s**2 + s**3))
+        assert_almost_equal(H.real, expected.real)
+        assert_almost_equal(H.imag, expected.imag)
+
+    def test_from_zpk(self):
+        # 4th order low-pass filter: H(s) = 1 / (s + 1)
+        system = lti([],[-1]*4,[1])
+        w = [0.1, 1, 10, 100]
+        w, H = freqresp(system, w=w)
+        s = w * 1j
+        expected = 1 / (s + 1)**4
+        assert_almost_equal(H.real, expected.real)
+        assert_almost_equal(H.imag, expected.imag)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_max_len_seq.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_max_len_seq.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4e79969974fb4ba376bbf4d935a7a94e3064a5a
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_max_len_seq.py
@@ -0,0 +1,65 @@
+import numpy as np
+from numpy.testing import assert_allclose, assert_array_equal
+from pytest import raises as assert_raises
+
+from numpy.fft import fft, ifft
+
+from scipy.signal import max_len_seq
+
+
+class TestMLS:
+
+    def test_mls_inputs(self):
+        # can't all be zero state
+        assert_raises(ValueError, max_len_seq,
+                      10, state=np.zeros(10))
+        # wrong size state
+        assert_raises(ValueError, max_len_seq, 10,
+                      state=np.ones(3))
+        # wrong length
+        assert_raises(ValueError, max_len_seq, 10, length=-1)
+        assert_array_equal(max_len_seq(10, length=0)[0], [])
+        # unknown taps
+        assert_raises(ValueError, max_len_seq, 64)
+        # bad taps
+        assert_raises(ValueError, max_len_seq, 10, taps=[-1, 1])
+
+    def test_mls_output(self):
+        # define some alternate working taps
+        alt_taps = {2: [1], 3: [2], 4: [3], 5: [4, 3, 2], 6: [5, 4, 1], 7: [4],
+                    8: [7, 5, 3]}
+        # assume the other bit levels work, too slow to test higher orders...
+        for nbits in range(2, 8):
+            for state in [None, np.round(np.random.rand(nbits))]:
+                for taps in [None, alt_taps[nbits]]:
+                    if state is not None and np.all(state == 0):
+                        state[0] = 1  # they can't all be zero
+                    orig_m = max_len_seq(nbits, state=state,
+                                         taps=taps)[0]
+                    m = 2. * orig_m - 1.  # convert to +/- 1 representation
+                    # First, make sure we got all 1's or -1
+                    err_msg = "mls had non binary terms"
+                    assert_array_equal(np.abs(m), np.ones_like(m),
+                                       err_msg=err_msg)
+                    # Test via circular cross-correlation, which is just mult.
+                    # in the frequency domain with one signal conjugated
+                    tester = np.real(ifft(fft(m) * np.conj(fft(m))))
+                    out_len = 2**nbits - 1
+                    # impulse amplitude == test_len
+                    err_msg = "mls impulse has incorrect value"
+                    assert_allclose(tester[0], out_len, err_msg=err_msg)
+                    # steady-state is -1
+                    err_msg = "mls steady-state has incorrect value"
+                    assert_allclose(tester[1:], np.full(out_len - 1, -1),
+                                    err_msg=err_msg)
+                    # let's do the split thing using a couple options
+                    for n in (1, 2**(nbits - 1)):
+                        m1, s1 = max_len_seq(nbits, state=state, taps=taps,
+                                             length=n)
+                        m2, s2 = max_len_seq(nbits, state=s1, taps=taps,
+                                             length=1)
+                        m3, s3 = max_len_seq(nbits, state=s2, taps=taps,
+                                             length=out_len - n - 1)
+                        new_m = np.concatenate((m1, m2, m3))
+                        assert_array_equal(orig_m, new_m)
+
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_peak_finding.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_peak_finding.py
new file mode 100644
index 0000000000000000000000000000000000000000..77380c5496364745775b0a3b6fcf149e72722398
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_peak_finding.py
@@ -0,0 +1,891 @@
+import copy
+
+import numpy as np
+from numpy.testing import (
+    assert_,
+    assert_equal,
+    assert_allclose,
+    assert_array_equal
+)
+import pytest
+from pytest import raises, warns
+
+from scipy.signal._peak_finding import (
+    argrelmax,
+    argrelmin,
+    peak_prominences,
+    peak_widths,
+    _unpack_condition_args,
+    find_peaks,
+    find_peaks_cwt,
+    _identify_ridge_lines
+)
+from scipy.signal.windows import gaussian
+from scipy.signal._peak_finding_utils import _local_maxima_1d, PeakPropertyWarning
+
+
+def _gen_gaussians(center_locs, sigmas, total_length):
+    xdata = np.arange(0, total_length).astype(float)
+    out_data = np.zeros(total_length, dtype=float)
+    for ind, sigma in enumerate(sigmas):
+        tmp = (xdata - center_locs[ind]) / sigma
+        out_data += np.exp(-(tmp**2))
+    return out_data
+
+
+def _gen_gaussians_even(sigmas, total_length):
+    num_peaks = len(sigmas)
+    delta = total_length / (num_peaks + 1)
+    center_locs = np.linspace(delta, total_length - delta, num=num_peaks).astype(int)
+    out_data = _gen_gaussians(center_locs, sigmas, total_length)
+    return out_data, center_locs
+
+
+def _gen_ridge_line(start_locs, max_locs, length, distances, gaps):
+    """
+    Generate coordinates for a ridge line.
+
+    Will be a series of coordinates, starting a start_loc (length 2).
+    The maximum distance between any adjacent columns will be
+    `max_distance`, the max distance between adjacent rows
+    will be `map_gap'.
+
+    `max_locs` should be the size of the intended matrix. The
+    ending coordinates are guaranteed to be less than `max_locs`,
+    although they may not approach `max_locs` at all.
+    """
+
+    def keep_bounds(num, max_val):
+        out = max(num, 0)
+        out = min(out, max_val)
+        return out
+
+    gaps = copy.deepcopy(gaps)
+    distances = copy.deepcopy(distances)
+
+    locs = np.zeros([length, 2], dtype=int)
+    locs[0, :] = start_locs
+    total_length = max_locs[0] - start_locs[0] - sum(gaps)
+    if total_length < length:
+        raise ValueError('Cannot generate ridge line according to constraints')
+    dist_int = length / len(distances) - 1
+    gap_int = length / len(gaps) - 1
+    for ind in range(1, length):
+        nextcol = locs[ind - 1, 1]
+        nextrow = locs[ind - 1, 0] + 1
+        if (ind % dist_int == 0) and (len(distances) > 0):
+            nextcol += ((-1)**ind)*distances.pop()
+        if (ind % gap_int == 0) and (len(gaps) > 0):
+            nextrow += gaps.pop()
+        nextrow = keep_bounds(nextrow, max_locs[0])
+        nextcol = keep_bounds(nextcol, max_locs[1])
+        locs[ind, :] = [nextrow, nextcol]
+
+    return [locs[:, 0], locs[:, 1]]
+
+
+class TestLocalMaxima1d:
+
+    def test_empty(self):
+        """Test with empty signal."""
+        x = np.array([], dtype=np.float64)
+        for array in _local_maxima_1d(x):
+            assert_equal(array, np.array([]))
+            assert_(array.base is None)
+
+    def test_linear(self):
+        """Test with linear signal."""
+        x = np.linspace(0, 100)
+        for array in _local_maxima_1d(x):
+            assert_equal(array, np.array([]))
+            assert_(array.base is None)
+
+    def test_simple(self):
+        """Test with simple signal."""
+        x = np.linspace(-10, 10, 50)
+        x[2::3] += 1
+        expected = np.arange(2, 50, 3)
+        for array in _local_maxima_1d(x):
+            # For plateaus of size 1, the edges are identical with the
+            # midpoints
+            assert_equal(array, expected)
+            assert_(array.base is None)
+
+    def test_flat_maxima(self):
+        """Test if flat maxima are detected correctly."""
+        x = np.array([-1.3, 0, 1, 0, 2, 2, 0, 3, 3, 3, 2.99, 4, 4, 4, 4, -10,
+                      -5, -5, -5, -5, -5, -10])
+        midpoints, left_edges, right_edges = _local_maxima_1d(x)
+        assert_equal(midpoints, np.array([2, 4, 8, 12, 18]))
+        assert_equal(left_edges, np.array([2, 4, 7, 11, 16]))
+        assert_equal(right_edges, np.array([2, 5, 9, 14, 20]))
+
+    @pytest.mark.parametrize('x', [
+        np.array([1., 0, 2]),
+        np.array([3., 3, 0, 4, 4]),
+        np.array([5., 5, 5, 0, 6, 6, 6]),
+    ])
+    def test_signal_edges(self, x):
+        """Test if behavior on signal edges is correct."""
+        for array in _local_maxima_1d(x):
+            assert_equal(array, np.array([]))
+            assert_(array.base is None)
+
+    def test_exceptions(self):
+        """Test input validation and raised exceptions."""
+        with raises(ValueError, match="wrong number of dimensions"):
+            _local_maxima_1d(np.ones((1, 1)))
+        with raises(ValueError, match="expected 'const float64_t'"):
+            _local_maxima_1d(np.ones(1, dtype=int))
+        with raises(TypeError, match="list"):
+            _local_maxima_1d([1., 2.])
+        with raises(TypeError, match="'x' must not be None"):
+            _local_maxima_1d(None)
+
+
+class TestRidgeLines:
+
+    def test_empty(self):
+        test_matr = np.zeros([20, 100])
+        lines = _identify_ridge_lines(test_matr, np.full(20, 2), 1)
+        assert_(len(lines) == 0)
+
+    def test_minimal(self):
+        test_matr = np.zeros([20, 100])
+        test_matr[0, 10] = 1
+        lines = _identify_ridge_lines(test_matr, np.full(20, 2), 1)
+        assert_(len(lines) == 1)
+
+        test_matr = np.zeros([20, 100])
+        test_matr[0:2, 10] = 1
+        lines = _identify_ridge_lines(test_matr, np.full(20, 2), 1)
+        assert_(len(lines) == 1)
+
+    def test_single_pass(self):
+        distances = [0, 1, 2, 5]
+        gaps = [0, 1, 2, 0, 1]
+        test_matr = np.zeros([20, 50]) + 1e-12
+        length = 12
+        line = _gen_ridge_line([0, 25], test_matr.shape, length, distances, gaps)
+        test_matr[line[0], line[1]] = 1
+        max_distances = np.full(20, max(distances))
+        identified_lines = _identify_ridge_lines(test_matr,
+                                                 max_distances,
+                                                 max(gaps) + 1)
+        assert_array_equal(identified_lines, [line])
+
+    def test_single_bigdist(self):
+        distances = [0, 1, 2, 5]
+        gaps = [0, 1, 2, 4]
+        test_matr = np.zeros([20, 50])
+        length = 12
+        line = _gen_ridge_line([0, 25], test_matr.shape, length, distances, gaps)
+        test_matr[line[0], line[1]] = 1
+        max_dist = 3
+        max_distances = np.full(20, max_dist)
+        #This should get 2 lines, since the distance is too large
+        identified_lines = _identify_ridge_lines(test_matr,
+                                                 max_distances,
+                                                 max(gaps) + 1)
+        assert_(len(identified_lines) == 2)
+
+        for iline in identified_lines:
+            adists = np.diff(iline[1])
+            np.testing.assert_array_less(np.abs(adists), max_dist)
+
+            agaps = np.diff(iline[0])
+            np.testing.assert_array_less(np.abs(agaps), max(gaps) + 0.1)
+
+    def test_single_biggap(self):
+        distances = [0, 1, 2, 5]
+        max_gap = 3
+        gaps = [0, 4, 2, 1]
+        test_matr = np.zeros([20, 50])
+        length = 12
+        line = _gen_ridge_line([0, 25], test_matr.shape, length, distances, gaps)
+        test_matr[line[0], line[1]] = 1
+        max_dist = 6
+        max_distances = np.full(20, max_dist)
+        #This should get 2 lines, since the gap is too large
+        identified_lines = _identify_ridge_lines(test_matr, max_distances, max_gap)
+        assert_(len(identified_lines) == 2)
+
+        for iline in identified_lines:
+            adists = np.diff(iline[1])
+            np.testing.assert_array_less(np.abs(adists), max_dist)
+
+            agaps = np.diff(iline[0])
+            np.testing.assert_array_less(np.abs(agaps), max(gaps) + 0.1)
+
+    def test_single_biggaps(self):
+        distances = [0]
+        max_gap = 1
+        gaps = [3, 6]
+        test_matr = np.zeros([50, 50])
+        length = 30
+        line = _gen_ridge_line([0, 25], test_matr.shape, length, distances, gaps)
+        test_matr[line[0], line[1]] = 1
+        max_dist = 1
+        max_distances = np.full(50, max_dist)
+        #This should get 3 lines, since the gaps are too large
+        identified_lines = _identify_ridge_lines(test_matr, max_distances, max_gap)
+        assert_(len(identified_lines) == 3)
+
+        for iline in identified_lines:
+            adists = np.diff(iline[1])
+            np.testing.assert_array_less(np.abs(adists), max_dist)
+
+            agaps = np.diff(iline[0])
+            np.testing.assert_array_less(np.abs(agaps), max(gaps) + 0.1)
+
+
+class TestArgrel:
+
+    def test_empty(self):
+        # Regression test for gh-2832.
+        # When there are no relative extrema, make sure that
+        # the number of empty arrays returned matches the
+        # dimension of the input.
+
+        empty_array = np.array([], dtype=int)
+
+        z1 = np.zeros(5)
+
+        i = argrelmin(z1)
+        assert_equal(len(i), 1)
+        assert_array_equal(i[0], empty_array)
+
+        z2 = np.zeros((3,5))
+
+        row, col = argrelmin(z2, axis=0)
+        assert_array_equal(row, empty_array)
+        assert_array_equal(col, empty_array)
+
+        row, col = argrelmin(z2, axis=1)
+        assert_array_equal(row, empty_array)
+        assert_array_equal(col, empty_array)
+
+    def test_basic(self):
+        # Note: the docstrings for the argrel{min,max,extrema} functions
+        # do not give a guarantee of the order of the indices, so we'll
+        # sort them before testing.
+
+        x = np.array([[1, 2, 2, 3, 2],
+                      [2, 1, 2, 2, 3],
+                      [3, 2, 1, 2, 2],
+                      [2, 3, 2, 1, 2],
+                      [1, 2, 3, 2, 1]])
+
+        row, col = argrelmax(x, axis=0)
+        order = np.argsort(row)
+        assert_equal(row[order], [1, 2, 3])
+        assert_equal(col[order], [4, 0, 1])
+
+        row, col = argrelmax(x, axis=1)
+        order = np.argsort(row)
+        assert_equal(row[order], [0, 3, 4])
+        assert_equal(col[order], [3, 1, 2])
+
+        row, col = argrelmin(x, axis=0)
+        order = np.argsort(row)
+        assert_equal(row[order], [1, 2, 3])
+        assert_equal(col[order], [1, 2, 3])
+
+        row, col = argrelmin(x, axis=1)
+        order = np.argsort(row)
+        assert_equal(row[order], [1, 2, 3])
+        assert_equal(col[order], [1, 2, 3])
+
+    def test_highorder(self):
+        order = 2
+        sigmas = [1.0, 2.0, 10.0, 5.0, 15.0]
+        test_data, act_locs = _gen_gaussians_even(sigmas, 500)
+        test_data[act_locs + order] = test_data[act_locs]*0.99999
+        test_data[act_locs - order] = test_data[act_locs]*0.99999
+        rel_max_locs = argrelmax(test_data, order=order, mode='clip')[0]
+
+        assert_(len(rel_max_locs) == len(act_locs))
+        assert_((rel_max_locs == act_locs).all())
+
+    def test_2d_gaussians(self):
+        sigmas = [1.0, 2.0, 10.0]
+        test_data, act_locs = _gen_gaussians_even(sigmas, 100)
+        rot_factor = 20
+        rot_range = np.arange(0, len(test_data)) - rot_factor
+        test_data_2 = np.vstack([test_data, test_data[rot_range]])
+        rel_max_rows, rel_max_cols = argrelmax(test_data_2, axis=1, order=1)
+
+        for rw in range(0, test_data_2.shape[0]):
+            inds = (rel_max_rows == rw)
+
+            assert_(len(rel_max_cols[inds]) == len(act_locs))
+            assert_((act_locs == (rel_max_cols[inds] - rot_factor*rw)).all())
+
+
+class TestPeakProminences:
+
+    def test_empty(self):
+        """
+        Test if an empty array is returned if no peaks are provided.
+        """
+        out = peak_prominences([1, 2, 3], [])
+        for arr, dtype in zip(out, [np.float64, np.intp, np.intp]):
+            assert_(arr.size == 0)
+            assert_(arr.dtype == dtype)
+
+        out = peak_prominences([], [])
+        for arr, dtype in zip(out, [np.float64, np.intp, np.intp]):
+            assert_(arr.size == 0)
+            assert_(arr.dtype == dtype)
+
+    def test_basic(self):
+        """
+        Test if height of prominences is correctly calculated in signal with
+        rising baseline (peak widths are 1 sample).
+        """
+        # Prepare basic signal
+        x = np.array([-1, 1.2, 1.2, 1, 3.2, 1.3, 2.88, 2.1])
+        peaks = np.array([1, 2, 4, 6])
+        lbases = np.array([0, 0, 0, 5])
+        rbases = np.array([3, 3, 5, 7])
+        proms = x[peaks] - np.max([x[lbases], x[rbases]], axis=0)
+        # Test if calculation matches handcrafted result
+        out = peak_prominences(x, peaks)
+        assert_equal(out[0], proms)
+        assert_equal(out[1], lbases)
+        assert_equal(out[2], rbases)
+
+    def test_edge_cases(self):
+        """
+        Test edge cases.
+        """
+        # Peaks have same height, prominence and bases
+        x = [0, 2, 1, 2, 1, 2, 0]
+        peaks = [1, 3, 5]
+        proms, lbases, rbases = peak_prominences(x, peaks)
+        assert_equal(proms, [2, 2, 2])
+        assert_equal(lbases, [0, 0, 0])
+        assert_equal(rbases, [6, 6, 6])
+
+        # Peaks have same height & prominence but different bases
+        x = [0, 1, 0, 1, 0, 1, 0]
+        peaks = np.array([1, 3, 5])
+        proms, lbases, rbases = peak_prominences(x, peaks)
+        assert_equal(proms, [1, 1, 1])
+        assert_equal(lbases, peaks - 1)
+        assert_equal(rbases, peaks + 1)
+
+    def test_non_contiguous(self):
+        """
+        Test with non-C-contiguous input arrays.
+        """
+        x = np.repeat([-9, 9, 9, 0, 3, 1], 2)
+        peaks = np.repeat([1, 2, 4], 2)
+        proms, lbases, rbases = peak_prominences(x[::2], peaks[::2])
+        assert_equal(proms, [9, 9, 2])
+        assert_equal(lbases, [0, 0, 3])
+        assert_equal(rbases, [3, 3, 5])
+
+    def test_wlen(self):
+        """
+        Test if wlen actually shrinks the evaluation range correctly.
+        """
+        x = [0, 1, 2, 3, 1, 0, -1]
+        peak = [3]
+        # Test rounding behavior of wlen
+        assert_equal(peak_prominences(x, peak), [3., 0, 6])
+        for wlen, i in [(8, 0), (7, 0), (6, 0), (5, 1), (3.2, 1), (3, 2), (1.1, 2)]:
+            assert_equal(peak_prominences(x, peak, wlen), [3. - i, 0 + i, 6 - i])
+
+    def test_exceptions(self):
+        """
+        Verify that exceptions and warnings are raised.
+        """
+        # x with dimension > 1
+        with raises(ValueError, match='1-D array'):
+            peak_prominences([[0, 1, 1, 0]], [1, 2])
+        # peaks with dimension > 1
+        with raises(ValueError, match='1-D array'):
+            peak_prominences([0, 1, 1, 0], [[1, 2]])
+        # x with dimension < 1
+        with raises(ValueError, match='1-D array'):
+            peak_prominences(3, [0,])
+
+        # empty x with supplied
+        with raises(ValueError, match='not a valid index'):
+            peak_prominences([], [0])
+        # invalid indices with non-empty x
+        for p in [-100, -1, 3, 1000]:
+            with raises(ValueError, match='not a valid index'):
+                peak_prominences([1, 0, 2], [p])
+
+        # peaks is not cast-able to np.intp
+        with raises(TypeError, match='cannot safely cast'):
+            peak_prominences([0, 1, 1, 0], [1.1, 2.3])
+
+        # wlen < 3
+        with raises(ValueError, match='wlen'):
+            peak_prominences(np.arange(10), [3, 5], wlen=1)
+
+    def test_warnings(self):
+        """
+        Verify that appropriate warnings are raised.
+        """
+        msg = "some peaks have a prominence of 0"
+        for p in [0, 1, 2]:
+            with warns(PeakPropertyWarning, match=msg):
+                peak_prominences([1, 0, 2], [p,])
+        with warns(PeakPropertyWarning, match=msg):
+            peak_prominences([0, 1, 1, 1, 0], [2], wlen=2)
+
+
+class TestPeakWidths:
+
+    def test_empty(self):
+        """
+        Test if an empty array is returned if no peaks are provided.
+        """
+        widths = peak_widths([], [])[0]
+        assert_(isinstance(widths, np.ndarray))
+        assert_equal(widths.size, 0)
+        widths = peak_widths([1, 2, 3], [])[0]
+        assert_(isinstance(widths, np.ndarray))
+        assert_equal(widths.size, 0)
+        out = peak_widths([], [])
+        for arr in out:
+            assert_(isinstance(arr, np.ndarray))
+            assert_equal(arr.size, 0)
+
+    @pytest.mark.filterwarnings("ignore:some peaks have a width of 0")
+    def test_basic(self):
+        """
+        Test a simple use case with easy to verify results at different relative
+        heights.
+        """
+        x = np.array([1, 0, 1, 2, 1, 0, -1])
+        prominence = 2
+        for rel_height, width_true, lip_true, rip_true in [
+            (0., 0., 3., 3.),  # raises warning
+            (0.25, 1., 2.5, 3.5),
+            (0.5, 2., 2., 4.),
+            (0.75, 3., 1.5, 4.5),
+            (1., 4., 1., 5.),
+            (2., 5., 1., 6.),
+            (3., 5., 1., 6.)
+        ]:
+            width_calc, height, lip_calc, rip_calc = peak_widths(
+                x, [3], rel_height)
+            assert_allclose(width_calc, width_true)
+            assert_allclose(height, 2 - rel_height * prominence)
+            assert_allclose(lip_calc, lip_true)
+            assert_allclose(rip_calc, rip_true)
+
+    def test_non_contiguous(self):
+        """
+        Test with non-C-contiguous input arrays.
+        """
+        x = np.repeat([0, 100, 50], 4)
+        peaks = np.repeat([1], 3)
+        result = peak_widths(x[::4], peaks[::3])
+        assert_equal(result, [0.75, 75, 0.75, 1.5])
+
+    def test_exceptions(self):
+        """
+        Verify that argument validation works as intended.
+        """
+        with raises(ValueError, match='1-D array'):
+            # x with dimension > 1
+            peak_widths(np.zeros((3, 4)), np.ones(3))
+        with raises(ValueError, match='1-D array'):
+            # x with dimension < 1
+            peak_widths(3, [0])
+        with raises(ValueError, match='1-D array'):
+            # peaks with dimension > 1
+            peak_widths(np.arange(10), np.ones((3, 2), dtype=np.intp))
+        with raises(ValueError, match='1-D array'):
+            # peaks with dimension < 1
+            peak_widths(np.arange(10), 3)
+        with raises(ValueError, match='not a valid index'):
+            # peak pos exceeds x.size
+            peak_widths(np.arange(10), [8, 11])
+        with raises(ValueError, match='not a valid index'):
+            # empty x with peaks supplied
+            peak_widths([], [1, 2])
+        with raises(TypeError, match='cannot safely cast'):
+            # peak cannot be safely casted to intp
+            peak_widths(np.arange(10), [1.1, 2.3])
+        with raises(ValueError, match='rel_height'):
+            # rel_height is < 0
+            peak_widths([0, 1, 0, 1, 0], [1, 3], rel_height=-1)
+        with raises(TypeError, match='None'):
+            # prominence data contains None
+            peak_widths([1, 2, 1], [1], prominence_data=(None, None, None))
+
+    def test_warnings(self):
+        """
+        Verify that appropriate warnings are raised.
+        """
+        msg = "some peaks have a width of 0"
+        with warns(PeakPropertyWarning, match=msg):
+            # Case: rel_height is 0
+            peak_widths([0, 1, 0], [1], rel_height=0)
+        with warns(PeakPropertyWarning, match=msg):
+            # Case: prominence is 0 and bases are identical
+            peak_widths(
+                [0, 1, 1, 1, 0], [2],
+                prominence_data=(np.array([0.], np.float64),
+                                 np.array([2], np.intp),
+                                 np.array([2], np.intp))
+            )
+
+    def test_mismatching_prominence_data(self):
+        """Test with mismatching peak and / or prominence data."""
+        x = [0, 1, 0]
+        peak = [1]
+        for i, (prominences, left_bases, right_bases) in enumerate([
+            ((1.,), (-1,), (2,)),  # left base not in x
+            ((1.,), (0,), (3,)),  # right base not in x
+            ((1.,), (2,), (0,)),  # swapped bases same as peak
+            ((1., 1.), (0, 0), (2, 2)),  # array shapes don't match peaks
+            ((1., 1.), (0,), (2,)),  # arrays with different shapes
+            ((1.,), (0, 0), (2,)),  # arrays with different shapes
+            ((1.,), (0,), (2, 2))  # arrays with different shapes
+        ]):
+            # Make sure input is matches output of signal.peak_prominences
+            prominence_data = (np.array(prominences, dtype=np.float64),
+                               np.array(left_bases, dtype=np.intp),
+                               np.array(right_bases, dtype=np.intp))
+            # Test for correct exception
+            if i < 3:
+                match = "prominence data is invalid for peak"
+            else:
+                match = "arrays in `prominence_data` must have the same shape"
+            with raises(ValueError, match=match):
+                peak_widths(x, peak, prominence_data=prominence_data)
+
+    @pytest.mark.filterwarnings("ignore:some peaks have a width of 0")
+    def test_intersection_rules(self):
+        """Test if x == eval_height counts as an intersection."""
+        # Flatt peak with two possible intersection points if evaluated at 1
+        x = [0, 1, 2, 1, 3, 3, 3, 1, 2, 1, 0]
+        # relative height is 0 -> width is 0 as well, raises warning
+        assert_allclose(peak_widths(x, peaks=[5], rel_height=0),
+                        [(0.,), (3.,), (5.,), (5.,)])
+        # width_height == x counts as intersection -> nearest 1 is chosen
+        assert_allclose(peak_widths(x, peaks=[5], rel_height=2/3),
+                        [(4.,), (1.,), (3.,), (7.,)])
+
+
+def test_unpack_condition_args():
+    """
+    Verify parsing of condition arguments for `scipy.signal.find_peaks` function.
+    """
+    x = np.arange(10)
+    amin_true = x
+    amax_true = amin_true + 10
+    peaks = amin_true[1::2]
+
+    # Test unpacking with None or interval
+    assert_((None, None) == _unpack_condition_args((None, None), x, peaks))
+    assert_((1, None) == _unpack_condition_args(1, x, peaks))
+    assert_((1, None) == _unpack_condition_args((1, None), x, peaks))
+    assert_((None, 2) == _unpack_condition_args((None, 2), x, peaks))
+    assert_((3., 4.5) == _unpack_condition_args((3., 4.5), x, peaks))
+
+    # Test if borders are correctly reduced with `peaks`
+    amin_calc, amax_calc = _unpack_condition_args((amin_true, amax_true), x, peaks)
+    assert_equal(amin_calc, amin_true[peaks])
+    assert_equal(amax_calc, amax_true[peaks])
+
+    # Test raises if array borders don't match x
+    with raises(ValueError, match="array size of lower"):
+        _unpack_condition_args(amin_true, np.arange(11), peaks)
+    with raises(ValueError, match="array size of upper"):
+        _unpack_condition_args((None, amin_true), np.arange(11), peaks)
+
+
+class TestFindPeaks:
+
+    # Keys of optionally returned properties
+    property_keys = {'peak_heights', 'left_thresholds', 'right_thresholds',
+                     'prominences', 'left_bases', 'right_bases', 'widths',
+                     'width_heights', 'left_ips', 'right_ips'}
+
+    def test_constant(self):
+        """
+        Test behavior for signal without local maxima.
+        """
+        open_interval = (None, None)
+        peaks, props = find_peaks(np.ones(10),
+                                  height=open_interval, threshold=open_interval,
+                                  prominence=open_interval, width=open_interval)
+        assert_(peaks.size == 0)
+        for key in self.property_keys:
+            assert_(props[key].size == 0)
+
+    def test_plateau_size(self):
+        """
+        Test plateau size condition for peaks.
+        """
+        # Prepare signal with peaks with peak_height == plateau_size
+        plateau_sizes = np.array([1, 2, 3, 4, 8, 20, 111])
+        x = np.zeros(plateau_sizes.size * 2 + 1)
+        x[1::2] = plateau_sizes
+        repeats = np.ones(x.size, dtype=int)
+        repeats[1::2] = x[1::2]
+        x = np.repeat(x, repeats)
+
+        # Test full output
+        peaks, props = find_peaks(x, plateau_size=(None, None))
+        assert_equal(peaks, [1, 3, 7, 11, 18, 33, 100])
+        assert_equal(props["plateau_sizes"], plateau_sizes)
+        assert_equal(props["left_edges"], peaks - (plateau_sizes - 1) // 2)
+        assert_equal(props["right_edges"], peaks + plateau_sizes // 2)
+
+        # Test conditions
+        assert_equal(find_peaks(x, plateau_size=4)[0], [11, 18, 33, 100])
+        assert_equal(find_peaks(x, plateau_size=(None, 3.5))[0], [1, 3, 7])
+        assert_equal(find_peaks(x, plateau_size=(5, 50))[0], [18, 33])
+
+    def test_height_condition(self):
+        """
+        Test height condition for peaks.
+        """
+        x = (0., 1/3, 0., 2.5, 0, 4., 0)
+        peaks, props = find_peaks(x, height=(None, None))
+        assert_equal(peaks, np.array([1, 3, 5]))
+        assert_equal(props['peak_heights'], np.array([1/3, 2.5, 4.]))
+        assert_equal(find_peaks(x, height=0.5)[0], np.array([3, 5]))
+        assert_equal(find_peaks(x, height=(None, 3))[0], np.array([1, 3]))
+        assert_equal(find_peaks(x, height=(2, 3))[0], np.array([3]))
+
+    def test_threshold_condition(self):
+        """
+        Test threshold condition for peaks.
+        """
+        x = (0, 2, 1, 4, -1)
+        peaks, props = find_peaks(x, threshold=(None, None))
+        assert_equal(peaks, np.array([1, 3]))
+        assert_equal(props['left_thresholds'], np.array([2, 3]))
+        assert_equal(props['right_thresholds'], np.array([1, 5]))
+        assert_equal(find_peaks(x, threshold=2)[0], np.array([3]))
+        assert_equal(find_peaks(x, threshold=3.5)[0], np.array([]))
+        assert_equal(find_peaks(x, threshold=(None, 5))[0], np.array([1, 3]))
+        assert_equal(find_peaks(x, threshold=(None, 4))[0], np.array([1]))
+        assert_equal(find_peaks(x, threshold=(2, 4))[0], np.array([]))
+
+    def test_distance_condition(self):
+        """
+        Test distance condition for peaks.
+        """
+        # Peaks of different height with constant distance 3
+        peaks_all = np.arange(1, 21, 3)
+        x = np.zeros(21)
+        x[peaks_all] += np.linspace(1, 2, peaks_all.size)
+
+        # Test if peaks with "minimal" distance are still selected (distance = 3)
+        assert_equal(find_peaks(x, distance=3)[0], peaks_all)
+
+        # Select every second peak (distance > 3)
+        peaks_subset = find_peaks(x, distance=3.0001)[0]
+        # Test if peaks_subset is subset of peaks_all
+        assert_(
+            np.setdiff1d(peaks_subset, peaks_all, assume_unique=True).size == 0
+        )
+        # Test if every second peak was removed
+        assert_equal(np.diff(peaks_subset), 6)
+
+        # Test priority of peak removal
+        x = [-2, 1, -1, 0, -3]
+        peaks_subset = find_peaks(x, distance=10)[0]  # use distance > x size
+        assert_(peaks_subset.size == 1 and peaks_subset[0] == 1)
+
+    def test_prominence_condition(self):
+        """
+        Test prominence condition for peaks.
+        """
+        x = np.linspace(0, 10, 100)
+        peaks_true = np.arange(1, 99, 2)
+        offset = np.linspace(1, 10, peaks_true.size)
+        x[peaks_true] += offset
+        prominences = x[peaks_true] - x[peaks_true + 1]
+        interval = (3, 9)
+        keep = np.nonzero(
+            (interval[0] <= prominences) & (prominences <= interval[1]))
+
+        peaks_calc, properties = find_peaks(x, prominence=interval)
+        assert_equal(peaks_calc, peaks_true[keep])
+        assert_equal(properties['prominences'], prominences[keep])
+        assert_equal(properties['left_bases'], 0)
+        assert_equal(properties['right_bases'], peaks_true[keep] + 1)
+
+    def test_width_condition(self):
+        """
+        Test width condition for peaks.
+        """
+        x = np.array([1, 0, 1, 2, 1, 0, -1, 4, 0])
+        peaks, props = find_peaks(x, width=(None, 2), rel_height=0.75)
+        assert_equal(peaks.size, 1)
+        assert_equal(peaks, 7)
+        assert_allclose(props['widths'], 1.35)
+        assert_allclose(props['width_heights'], 1.)
+        assert_allclose(props['left_ips'], 6.4)
+        assert_allclose(props['right_ips'], 7.75)
+
+    def test_properties(self):
+        """
+        Test returned properties.
+        """
+        open_interval = (None, None)
+        x = [0, 1, 0, 2, 1.5, 0, 3, 0, 5, 9]
+        peaks, props = find_peaks(x,
+                                  height=open_interval, threshold=open_interval,
+                                  prominence=open_interval, width=open_interval)
+        assert_(len(props) == len(self.property_keys))
+        for key in self.property_keys:
+            assert_(peaks.size == props[key].size)
+
+    def test_raises(self):
+        """
+        Test exceptions raised by function.
+        """
+        with raises(ValueError, match="1-D array"):
+            find_peaks(np.array(1))
+        with raises(ValueError, match="1-D array"):
+            find_peaks(np.ones((2, 2)))
+        with raises(ValueError, match="distance"):
+            find_peaks(np.arange(10), distance=-1)
+
+    @pytest.mark.filterwarnings("ignore:some peaks have a prominence of 0",
+                                "ignore:some peaks have a width of 0")
+    def test_wlen_smaller_plateau(self):
+        """
+        Test behavior of prominence and width calculation if the given window
+        length is smaller than a peak's plateau size.
+
+        Regression test for gh-9110.
+        """
+        peaks, props = find_peaks([0, 1, 1, 1, 0], prominence=(None, None),
+                                  width=(None, None), wlen=2)
+        assert_equal(peaks, 2)
+        assert_equal(props["prominences"], 0)
+        assert_equal(props["widths"], 0)
+        assert_equal(props["width_heights"], 1)
+        for key in ("left_bases", "right_bases", "left_ips", "right_ips"):
+            assert_equal(props[key], peaks)
+
+    @pytest.mark.parametrize("kwargs", [
+        {},
+        {"distance": 3.0},
+        {"prominence": (None, None)},
+        {"width": (None, 2)},
+
+    ])
+    def test_readonly_array(self, kwargs):
+        """
+        Test readonly arrays are accepted.
+        """
+        x = np.linspace(0, 10, 15)
+        x_readonly = x.copy()
+        x_readonly.flags.writeable = False
+
+        peaks, _ = find_peaks(x)
+        peaks_readonly, _ = find_peaks(x_readonly, **kwargs)
+
+        assert_allclose(peaks, peaks_readonly)
+
+
+class TestFindPeaksCwt:
+
+    def test_find_peaks_exact(self):
+        """
+        Generate a series of gaussians and attempt to find the peak locations.
+        """
+        sigmas = [5.0, 3.0, 10.0, 20.0, 10.0, 50.0]
+        num_points = 500
+        test_data, act_locs = _gen_gaussians_even(sigmas, num_points)
+        widths = np.arange(0.1, max(sigmas))
+        found_locs = find_peaks_cwt(test_data, widths, gap_thresh=2, min_snr=0,
+                                         min_length=None)
+        np.testing.assert_array_equal(found_locs, act_locs,
+                        "Found maximum locations did not equal those expected")
+
+    def test_find_peaks_withnoise(self):
+        """
+        Verify that peak locations are (approximately) found
+        for a series of gaussians with added noise.
+        """
+        sigmas = [5.0, 3.0, 10.0, 20.0, 10.0, 50.0]
+        num_points = 500
+        test_data, act_locs = _gen_gaussians_even(sigmas, num_points)
+        widths = np.arange(0.1, max(sigmas))
+        noise_amp = 0.07
+        np.random.seed(18181911)
+        test_data += (np.random.rand(num_points) - 0.5)*(2*noise_amp)
+        found_locs = find_peaks_cwt(test_data, widths, min_length=15,
+                                         gap_thresh=1, min_snr=noise_amp / 5)
+
+        np.testing.assert_equal(len(found_locs), len(act_locs), 'Different number' +
+                                'of peaks found than expected')
+        diffs = np.abs(found_locs - act_locs)
+        max_diffs = np.array(sigmas) / 5
+        np.testing.assert_array_less(diffs, max_diffs, 'Maximum location differed' +
+                                     'by more than %s' % (max_diffs))
+
+    def test_find_peaks_nopeak(self):
+        """
+        Verify that no peak is found in
+        data that's just noise.
+        """
+        noise_amp = 1.0
+        num_points = 100
+        np.random.seed(181819141)
+        test_data = (np.random.rand(num_points) - 0.5)*(2*noise_amp)
+        widths = np.arange(10, 50)
+        found_locs = find_peaks_cwt(test_data, widths, min_snr=5, noise_perc=30)
+        np.testing.assert_equal(len(found_locs), 0)
+
+    def test_find_peaks_with_non_default_wavelets(self):
+        x = gaussian(200, 2)
+        widths = np.array([1, 2, 3, 4])
+        a = find_peaks_cwt(x, widths, wavelet=gaussian)
+
+        np.testing.assert_equal(np.array([100]), a)
+
+    def test_find_peaks_window_size(self):
+        """
+        Verify that window_size is passed correctly to private function and
+        affects the result.
+        """
+        sigmas = [2.0, 2.0]
+        num_points = 1000
+        test_data, act_locs = _gen_gaussians_even(sigmas, num_points)
+        widths = np.arange(0.1, max(sigmas), 0.2)
+        noise_amp = 0.05
+        np.random.seed(18181911)
+        test_data += (np.random.rand(num_points) - 0.5)*(2*noise_amp)
+
+        # Possibly contrived negative region to throw off peak finding
+        # when window_size is too large
+        test_data[250:320] -= 1
+
+        found_locs = find_peaks_cwt(test_data, widths, gap_thresh=2, min_snr=3,
+                                    min_length=None, window_size=None)
+        with pytest.raises(AssertionError):
+            assert found_locs.size == act_locs.size
+
+        found_locs = find_peaks_cwt(test_data, widths, gap_thresh=2, min_snr=3,
+                                    min_length=None, window_size=20)
+        assert found_locs.size == act_locs.size
+
+    def test_find_peaks_with_one_width(self):
+        """
+        Verify that the `width` argument
+        in `find_peaks_cwt` can be a float
+        """
+        xs = np.arange(0, np.pi, 0.05)
+        test_data = np.sin(xs)
+        widths = 1
+        found_locs = find_peaks_cwt(test_data, widths)
+
+        np.testing.assert_equal(found_locs, 32)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_savitzky_golay.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_savitzky_golay.py
new file mode 100644
index 0000000000000000000000000000000000000000..fbbf370bf558612b7f866b252303b2cfdcb58b06
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_savitzky_golay.py
@@ -0,0 +1,358 @@
+import pytest
+import numpy as np
+from numpy.testing import (assert_allclose, assert_equal,
+                           assert_almost_equal, assert_array_equal,
+                           assert_array_almost_equal)
+
+from scipy.ndimage import convolve1d
+
+from scipy.signal import savgol_coeffs, savgol_filter
+from scipy.signal._savitzky_golay import _polyder
+
+
+def check_polyder(p, m, expected):
+    dp = _polyder(p, m)
+    assert_array_equal(dp, expected)
+
+
+def test_polyder():
+    cases = [
+        ([5], 0, [5]),
+        ([5], 1, [0]),
+        ([3, 2, 1], 0, [3, 2, 1]),
+        ([3, 2, 1], 1, [6, 2]),
+        ([3, 2, 1], 2, [6]),
+        ([3, 2, 1], 3, [0]),
+        ([[3, 2, 1], [5, 6, 7]], 0, [[3, 2, 1], [5, 6, 7]]),
+        ([[3, 2, 1], [5, 6, 7]], 1, [[6, 2], [10, 6]]),
+        ([[3, 2, 1], [5, 6, 7]], 2, [[6], [10]]),
+        ([[3, 2, 1], [5, 6, 7]], 3, [[0], [0]]),
+    ]
+    for p, m, expected in cases:
+        check_polyder(np.array(p).T, m, np.array(expected).T)
+
+
+#--------------------------------------------------------------------
+# savgol_coeffs tests
+#--------------------------------------------------------------------
+
+def alt_sg_coeffs(window_length, polyorder, pos):
+    """This is an alternative implementation of the SG coefficients.
+
+    It uses numpy.polyfit and numpy.polyval. The results should be
+    equivalent to those of savgol_coeffs(), but this implementation
+    is slower.
+
+    window_length should be odd.
+
+    """
+    if pos is None:
+        pos = window_length // 2
+    t = np.arange(window_length)
+    unit = (t == pos).astype(int)
+    h = np.polyval(np.polyfit(t, unit, polyorder), t)
+    return h
+
+
+def test_sg_coeffs_trivial():
+    # Test a trivial case of savgol_coeffs: polyorder = window_length - 1
+    h = savgol_coeffs(1, 0)
+    assert_allclose(h, [1])
+
+    h = savgol_coeffs(3, 2)
+    assert_allclose(h, [0, 1, 0], atol=1e-10)
+
+    h = savgol_coeffs(5, 4)
+    assert_allclose(h, [0, 0, 1, 0, 0], atol=1e-10)
+
+    h = savgol_coeffs(5, 4, pos=1)
+    assert_allclose(h, [0, 0, 0, 1, 0], atol=1e-10)
+
+    h = savgol_coeffs(5, 4, pos=1, use='dot')
+    assert_allclose(h, [0, 1, 0, 0, 0], atol=1e-10)
+
+
+def compare_coeffs_to_alt(window_length, order):
+    # For the given window_length and order, compare the results
+    # of savgol_coeffs and alt_sg_coeffs for pos from 0 to window_length - 1.
+    # Also include pos=None.
+    for pos in [None] + list(range(window_length)):
+        h1 = savgol_coeffs(window_length, order, pos=pos, use='dot')
+        h2 = alt_sg_coeffs(window_length, order, pos=pos)
+        assert_allclose(h1, h2, atol=1e-10,
+                        err_msg=("window_length = %d, order = %d, pos = %s" %
+                                 (window_length, order, pos)))
+
+
+def test_sg_coeffs_compare():
+    # Compare savgol_coeffs() to alt_sg_coeffs().
+    for window_length in range(1, 8, 2):
+        for order in range(window_length):
+            compare_coeffs_to_alt(window_length, order)
+
+
+def test_sg_coeffs_exact():
+    polyorder = 4
+    window_length = 9
+    halflen = window_length // 2
+
+    x = np.linspace(0, 21, 43)
+    delta = x[1] - x[0]
+
+    # The data is a cubic polynomial.  We'll use an order 4
+    # SG filter, so the filtered values should equal the input data
+    # (except within half window_length of the edges).
+    y = 0.5 * x ** 3 - x
+    h = savgol_coeffs(window_length, polyorder)
+    y0 = convolve1d(y, h)
+    assert_allclose(y0[halflen:-halflen], y[halflen:-halflen])
+
+    # Check the same input, but use deriv=1.  dy is the exact result.
+    dy = 1.5 * x ** 2 - 1
+    h = savgol_coeffs(window_length, polyorder, deriv=1, delta=delta)
+    y1 = convolve1d(y, h)
+    assert_allclose(y1[halflen:-halflen], dy[halflen:-halflen])
+
+    # Check the same input, but use deriv=2. d2y is the exact result.
+    d2y = 3.0 * x
+    h = savgol_coeffs(window_length, polyorder, deriv=2, delta=delta)
+    y2 = convolve1d(y, h)
+    assert_allclose(y2[halflen:-halflen], d2y[halflen:-halflen])
+
+
+def test_sg_coeffs_deriv():
+    # The data in `x` is a sampled parabola, so using savgol_coeffs with an
+    # order 2 or higher polynomial should give exact results.
+    i = np.array([-2.0, 0.0, 2.0, 4.0, 6.0])
+    x = i ** 2 / 4
+    dx = i / 2
+    d2x = np.full_like(i, 0.5)
+    for pos in range(x.size):
+        coeffs0 = savgol_coeffs(5, 3, pos=pos, delta=2.0, use='dot')
+        assert_allclose(coeffs0.dot(x), x[pos], atol=1e-10)
+        coeffs1 = savgol_coeffs(5, 3, pos=pos, delta=2.0, use='dot', deriv=1)
+        assert_allclose(coeffs1.dot(x), dx[pos], atol=1e-10)
+        coeffs2 = savgol_coeffs(5, 3, pos=pos, delta=2.0, use='dot', deriv=2)
+        assert_allclose(coeffs2.dot(x), d2x[pos], atol=1e-10)
+
+
+def test_sg_coeffs_deriv_gt_polyorder():
+    """
+    If deriv > polyorder, the coefficients should be all 0.
+    This is a regression test for a bug where, e.g.,
+        savgol_coeffs(5, polyorder=1, deriv=2)
+    raised an error.
+    """
+    coeffs = savgol_coeffs(5, polyorder=1, deriv=2)
+    assert_array_equal(coeffs, np.zeros(5))
+    coeffs = savgol_coeffs(7, polyorder=4, deriv=6)
+    assert_array_equal(coeffs, np.zeros(7))
+
+
+def test_sg_coeffs_large():
+    # Test that for large values of window_length and polyorder the array of
+    # coefficients returned is symmetric. The aim is to ensure that
+    # no potential numeric overflow occurs.
+    coeffs0 = savgol_coeffs(31, 9)
+    assert_array_almost_equal(coeffs0, coeffs0[::-1])
+    coeffs1 = savgol_coeffs(31, 9, deriv=1)
+    assert_array_almost_equal(coeffs1, -coeffs1[::-1])
+
+# --------------------------------------------------------------------
+# savgol_coeffs tests for even window length
+# --------------------------------------------------------------------
+
+
+def test_sg_coeffs_even_window_length():
+    # Simple case - deriv=0, polyorder=0, 1
+    window_lengths = [4, 6, 8, 10, 12, 14, 16]
+    for length in window_lengths:
+        h_p_d = savgol_coeffs(length, 0, 0)
+        assert_allclose(h_p_d, 1/length)
+
+    # Verify with closed forms
+    # deriv=1, polyorder=1, 2
+    def h_p_d_closed_form_1(k, m):
+        return 6*(k - 0.5)/((2*m + 1)*m*(2*m - 1))
+
+    # deriv=2, polyorder=2
+    def h_p_d_closed_form_2(k, m):
+        numer = 15*(-4*m**2 + 1 + 12*(k - 0.5)**2)
+        denom = 4*(2*m + 1)*(m + 1)*m*(m - 1)*(2*m - 1)
+        return numer/denom
+
+    for length in window_lengths:
+        m = length//2
+        expected_output = [h_p_d_closed_form_1(k, m)
+                           for k in range(-m + 1, m + 1)][::-1]
+        actual_output = savgol_coeffs(length, 1, 1)
+        assert_allclose(expected_output, actual_output)
+        actual_output = savgol_coeffs(length, 2, 1)
+        assert_allclose(expected_output, actual_output)
+
+        expected_output = [h_p_d_closed_form_2(k, m)
+                           for k in range(-m + 1, m + 1)][::-1]
+        actual_output = savgol_coeffs(length, 2, 2)
+        assert_allclose(expected_output, actual_output)
+        actual_output = savgol_coeffs(length, 3, 2)
+        assert_allclose(expected_output, actual_output)
+
+#--------------------------------------------------------------------
+# savgol_filter tests
+#--------------------------------------------------------------------
+
+
+def test_sg_filter_trivial():
+    """ Test some trivial edge cases for savgol_filter()."""
+    x = np.array([1.0])
+    y = savgol_filter(x, 1, 0)
+    assert_equal(y, [1.0])
+
+    # Input is a single value. With a window length of 3 and polyorder 1,
+    # the value in y is from the straight-line fit of (-1,0), (0,3) and
+    # (1, 0) at 0. This is just the average of the three values, hence 1.0.
+    x = np.array([3.0])
+    y = savgol_filter(x, 3, 1, mode='constant')
+    assert_almost_equal(y, [1.0], decimal=15)
+
+    x = np.array([3.0])
+    y = savgol_filter(x, 3, 1, mode='nearest')
+    assert_almost_equal(y, [3.0], decimal=15)
+
+    x = np.array([1.0] * 3)
+    y = savgol_filter(x, 3, 1, mode='wrap')
+    assert_almost_equal(y, [1.0, 1.0, 1.0], decimal=15)
+
+
+def test_sg_filter_basic():
+    # Some basic test cases for savgol_filter().
+    x = np.array([1.0, 2.0, 1.0])
+    y = savgol_filter(x, 3, 1, mode='constant')
+    assert_allclose(y, [1.0, 4.0 / 3, 1.0])
+
+    y = savgol_filter(x, 3, 1, mode='mirror')
+    assert_allclose(y, [5.0 / 3, 4.0 / 3, 5.0 / 3])
+
+    y = savgol_filter(x, 3, 1, mode='wrap')
+    assert_allclose(y, [4.0 / 3, 4.0 / 3, 4.0 / 3])
+
+
+def test_sg_filter_2d():
+    x = np.array([[1.0, 2.0, 1.0],
+                  [2.0, 4.0, 2.0]])
+    expected = np.array([[1.0, 4.0 / 3, 1.0],
+                         [2.0, 8.0 / 3, 2.0]])
+    y = savgol_filter(x, 3, 1, mode='constant')
+    assert_allclose(y, expected)
+
+    y = savgol_filter(x.T, 3, 1, mode='constant', axis=0)
+    assert_allclose(y, expected.T)
+
+
+def test_sg_filter_interp_edges():
+    # Another test with low degree polynomial data, for which we can easily
+    # give the exact results. In this test, we use mode='interp', so
+    # savgol_filter should match the exact solution for the entire data set,
+    # including the edges.
+    t = np.linspace(-5, 5, 21)
+    delta = t[1] - t[0]
+    # Polynomial test data.
+    x = np.array([t,
+                  3 * t ** 2,
+                  t ** 3 - t])
+    dx = np.array([np.ones_like(t),
+                   6 * t,
+                   3 * t ** 2 - 1.0])
+    d2x = np.array([np.zeros_like(t),
+                    np.full_like(t, 6),
+                    6 * t])
+
+    window_length = 7
+
+    y = savgol_filter(x, window_length, 3, axis=-1, mode='interp')
+    assert_allclose(y, x, atol=1e-12)
+
+    y1 = savgol_filter(x, window_length, 3, axis=-1, mode='interp',
+                       deriv=1, delta=delta)
+    assert_allclose(y1, dx, atol=1e-12)
+
+    y2 = savgol_filter(x, window_length, 3, axis=-1, mode='interp',
+                       deriv=2, delta=delta)
+    assert_allclose(y2, d2x, atol=1e-12)
+
+    # Transpose everything, and test again with axis=0.
+
+    x = x.T
+    dx = dx.T
+    d2x = d2x.T
+
+    y = savgol_filter(x, window_length, 3, axis=0, mode='interp')
+    assert_allclose(y, x, atol=1e-12)
+
+    y1 = savgol_filter(x, window_length, 3, axis=0, mode='interp',
+                       deriv=1, delta=delta)
+    assert_allclose(y1, dx, atol=1e-12)
+
+    y2 = savgol_filter(x, window_length, 3, axis=0, mode='interp',
+                       deriv=2, delta=delta)
+    assert_allclose(y2, d2x, atol=1e-12)
+
+
+def test_sg_filter_interp_edges_3d():
+    # Test mode='interp' with a 3-D array.
+    t = np.linspace(-5, 5, 21)
+    delta = t[1] - t[0]
+    x1 = np.array([t, -t])
+    x2 = np.array([t ** 2, 3 * t ** 2 + 5])
+    x3 = np.array([t ** 3, 2 * t ** 3 + t ** 2 - 0.5 * t])
+    dx1 = np.array([np.ones_like(t), -np.ones_like(t)])
+    dx2 = np.array([2 * t, 6 * t])
+    dx3 = np.array([3 * t ** 2, 6 * t ** 2 + 2 * t - 0.5])
+
+    # z has shape (3, 2, 21)
+    z = np.array([x1, x2, x3])
+    dz = np.array([dx1, dx2, dx3])
+
+    y = savgol_filter(z, 7, 3, axis=-1, mode='interp', delta=delta)
+    assert_allclose(y, z, atol=1e-10)
+
+    dy = savgol_filter(z, 7, 3, axis=-1, mode='interp', deriv=1, delta=delta)
+    assert_allclose(dy, dz, atol=1e-10)
+
+    # z has shape (3, 21, 2)
+    z = np.array([x1.T, x2.T, x3.T])
+    dz = np.array([dx1.T, dx2.T, dx3.T])
+
+    y = savgol_filter(z, 7, 3, axis=1, mode='interp', delta=delta)
+    assert_allclose(y, z, atol=1e-10)
+
+    dy = savgol_filter(z, 7, 3, axis=1, mode='interp', deriv=1, delta=delta)
+    assert_allclose(dy, dz, atol=1e-10)
+
+    # z has shape (21, 3, 2)
+    z = z.swapaxes(0, 1).copy()
+    dz = dz.swapaxes(0, 1).copy()
+
+    y = savgol_filter(z, 7, 3, axis=0, mode='interp', delta=delta)
+    assert_allclose(y, z, atol=1e-10)
+
+    dy = savgol_filter(z, 7, 3, axis=0, mode='interp', deriv=1, delta=delta)
+    assert_allclose(dy, dz, atol=1e-10)
+
+
+def test_sg_filter_valid_window_length_3d():
+    """Tests that the window_length check is using the correct axis."""
+
+    x = np.ones((10, 20, 30))
+
+    savgol_filter(x, window_length=29, polyorder=3, mode='interp')
+
+    with pytest.raises(ValueError, match='window_length must be less than'):
+        # window_length is more than x.shape[-1].
+        savgol_filter(x, window_length=31, polyorder=3, mode='interp')
+
+    savgol_filter(x, window_length=9, polyorder=3, axis=0, mode='interp')
+
+    with pytest.raises(ValueError, match='window_length must be less than'):
+        # window_length is more than x.shape[0].
+        savgol_filter(x, window_length=11, polyorder=3, axis=0, mode='interp')
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_spectral.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_spectral.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed0af49b2ef8901f3c8b073f4d19def5578d0dbe
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_spectral.py
@@ -0,0 +1,1713 @@
+import sys
+
+import numpy as np
+from numpy.testing import (assert_, assert_approx_equal,
+                           assert_allclose, assert_array_equal, assert_equal,
+                           assert_array_almost_equal_nulp, suppress_warnings)
+import pytest
+from pytest import raises as assert_raises
+
+from scipy import signal
+from scipy.fft import fftfreq, rfftfreq, fft, irfft
+from scipy.integrate import trapezoid
+from scipy.signal import (periodogram, welch, lombscargle, coherence,
+                          spectrogram, check_COLA, check_NOLA)
+from scipy.signal.windows import hann
+from scipy.signal._spectral_py import _spectral_helper
+
+# Compare ShortTimeFFT.stft() / ShortTimeFFT.istft() with stft() / istft():
+from scipy.signal.tests._scipy_spectral_test_shim import stft_compare as stft
+from scipy.signal.tests._scipy_spectral_test_shim import istft_compare as istft
+from scipy.signal.tests._scipy_spectral_test_shim import csd_compare as csd
+
+
+class TestPeriodogram:
+    def test_real_onesided_even(self):
+        x = np.zeros(16)
+        x[0] = 1
+        f, p = periodogram(x)
+        assert_allclose(f, np.linspace(0, 0.5, 9))
+        q = np.ones(9)
+        q[0] = 0
+        q[-1] /= 2.0
+        q /= 8
+        assert_allclose(p, q)
+
+    def test_real_onesided_odd(self):
+        x = np.zeros(15)
+        x[0] = 1
+        f, p = periodogram(x)
+        assert_allclose(f, np.arange(8.0)/15.0)
+        q = np.ones(8)
+        q[0] = 0
+        q *= 2.0/15.0
+        assert_allclose(p, q, atol=1e-15)
+
+    def test_real_twosided(self):
+        x = np.zeros(16)
+        x[0] = 1
+        f, p = periodogram(x, return_onesided=False)
+        assert_allclose(f, fftfreq(16, 1.0))
+        q = np.full(16, 1/16.0)
+        q[0] = 0
+        assert_allclose(p, q)
+
+    def test_real_spectrum(self):
+        x = np.zeros(16)
+        x[0] = 1
+        f, p = periodogram(x, scaling='spectrum')
+        g, q = periodogram(x, scaling='density')
+        assert_allclose(f, np.linspace(0, 0.5, 9))
+        assert_allclose(p, q/16.0)
+
+    def test_integer_even(self):
+        x = np.zeros(16, dtype=int)
+        x[0] = 1
+        f, p = periodogram(x)
+        assert_allclose(f, np.linspace(0, 0.5, 9))
+        q = np.ones(9)
+        q[0] = 0
+        q[-1] /= 2.0
+        q /= 8
+        assert_allclose(p, q)
+
+    def test_integer_odd(self):
+        x = np.zeros(15, dtype=int)
+        x[0] = 1
+        f, p = periodogram(x)
+        assert_allclose(f, np.arange(8.0)/15.0)
+        q = np.ones(8)
+        q[0] = 0
+        q *= 2.0/15.0
+        assert_allclose(p, q, atol=1e-15)
+
+    def test_integer_twosided(self):
+        x = np.zeros(16, dtype=int)
+        x[0] = 1
+        f, p = periodogram(x, return_onesided=False)
+        assert_allclose(f, fftfreq(16, 1.0))
+        q = np.full(16, 1/16.0)
+        q[0] = 0
+        assert_allclose(p, q)
+
+    def test_complex(self):
+        x = np.zeros(16, np.complex128)
+        x[0] = 1.0 + 2.0j
+        f, p = periodogram(x, return_onesided=False)
+        assert_allclose(f, fftfreq(16, 1.0))
+        q = np.full(16, 5.0/16.0)
+        q[0] = 0
+        assert_allclose(p, q)
+
+    def test_unk_scaling(self):
+        assert_raises(ValueError, periodogram, np.zeros(4, np.complex128),
+                scaling='foo')
+
+    @pytest.mark.skipif(
+        sys.maxsize <= 2**32,
+        reason="On some 32-bit tolerance issue"
+    )
+    def test_nd_axis_m1(self):
+        x = np.zeros(20, dtype=np.float64)
+        x = x.reshape((2,1,10))
+        x[:,:,0] = 1.0
+        f, p = periodogram(x)
+        assert_array_equal(p.shape, (2, 1, 6))
+        assert_array_almost_equal_nulp(p[0,0,:], p[1,0,:], 60)
+        f0, p0 = periodogram(x[0,0,:])
+        assert_array_almost_equal_nulp(p0[np.newaxis,:], p[1,:], 60)
+
+    @pytest.mark.skipif(
+        sys.maxsize <= 2**32,
+        reason="On some 32-bit tolerance issue"
+    )
+    def test_nd_axis_0(self):
+        x = np.zeros(20, dtype=np.float64)
+        x = x.reshape((10,2,1))
+        x[0,:,:] = 1.0
+        f, p = periodogram(x, axis=0)
+        assert_array_equal(p.shape, (6,2,1))
+        assert_array_almost_equal_nulp(p[:,0,0], p[:,1,0], 60)
+        f0, p0 = periodogram(x[:,0,0])
+        assert_array_almost_equal_nulp(p0, p[:,1,0])
+
+    def test_window_external(self):
+        x = np.zeros(16)
+        x[0] = 1
+        f, p = periodogram(x, 10, 'hann')
+        win = signal.get_window('hann', 16)
+        fe, pe = periodogram(x, 10, win)
+        assert_array_almost_equal_nulp(p, pe)
+        assert_array_almost_equal_nulp(f, fe)
+        win_err = signal.get_window('hann', 32)
+        assert_raises(ValueError, periodogram, x,
+                      10, win_err)  # win longer than signal
+
+    def test_padded_fft(self):
+        x = np.zeros(16)
+        x[0] = 1
+        f, p = periodogram(x)
+        fp, pp = periodogram(x, nfft=32)
+        assert_allclose(f, fp[::2])
+        assert_allclose(p, pp[::2])
+        assert_array_equal(pp.shape, (17,))
+
+    def test_empty_input(self):
+        f, p = periodogram([])
+        assert_array_equal(f.shape, (0,))
+        assert_array_equal(p.shape, (0,))
+        for shape in [(0,), (3,0), (0,5,2)]:
+            f, p = periodogram(np.empty(shape))
+            assert_array_equal(f.shape, shape)
+            assert_array_equal(p.shape, shape)
+
+    def test_empty_input_other_axis(self):
+        for shape in [(3,0), (0,5,2)]:
+            f, p = periodogram(np.empty(shape), axis=1)
+            assert_array_equal(f.shape, shape)
+            assert_array_equal(p.shape, shape)
+
+    def test_short_nfft(self):
+        x = np.zeros(18)
+        x[0] = 1
+        f, p = periodogram(x, nfft=16)
+        assert_allclose(f, np.linspace(0, 0.5, 9))
+        q = np.ones(9)
+        q[0] = 0
+        q[-1] /= 2.0
+        q /= 8
+        assert_allclose(p, q)
+
+    def test_nfft_is_xshape(self):
+        x = np.zeros(16)
+        x[0] = 1
+        f, p = periodogram(x, nfft=16)
+        assert_allclose(f, np.linspace(0, 0.5, 9))
+        q = np.ones(9)
+        q[0] = 0
+        q[-1] /= 2.0
+        q /= 8
+        assert_allclose(p, q)
+
+    def test_real_onesided_even_32(self):
+        x = np.zeros(16, 'f')
+        x[0] = 1
+        f, p = periodogram(x)
+        assert_allclose(f, np.linspace(0, 0.5, 9))
+        q = np.ones(9, 'f')
+        q[0] = 0
+        q[-1] /= 2.0
+        q /= 8
+        assert_allclose(p, q)
+        assert_(p.dtype == q.dtype)
+
+    def test_real_onesided_odd_32(self):
+        x = np.zeros(15, 'f')
+        x[0] = 1
+        f, p = periodogram(x)
+        assert_allclose(f, np.arange(8.0)/15.0)
+        q = np.ones(8, 'f')
+        q[0] = 0
+        q *= 2.0/15.0
+        assert_allclose(p, q, atol=1e-7)
+        assert_(p.dtype == q.dtype)
+
+    def test_real_twosided_32(self):
+        x = np.zeros(16, 'f')
+        x[0] = 1
+        f, p = periodogram(x, return_onesided=False)
+        assert_allclose(f, fftfreq(16, 1.0))
+        q = np.full(16, 1/16.0, 'f')
+        q[0] = 0
+        assert_allclose(p, q)
+        assert_(p.dtype == q.dtype)
+
+    def test_complex_32(self):
+        x = np.zeros(16, 'F')
+        x[0] = 1.0 + 2.0j
+        f, p = periodogram(x, return_onesided=False)
+        assert_allclose(f, fftfreq(16, 1.0))
+        q = np.full(16, 5.0/16.0, 'f')
+        q[0] = 0
+        assert_allclose(p, q)
+        assert_(p.dtype == q.dtype)
+
+    def test_shorter_window_error(self):
+        x = np.zeros(16)
+        x[0] = 1
+        win = signal.get_window('hann', 10)
+        expected_msg = ('the size of the window must be the same size '
+                        'of the input on the specified axis')
+        with assert_raises(ValueError, match=expected_msg):
+            periodogram(x, window=win)
+
+
+class TestWelch:
+    def test_real_onesided_even(self):
+        x = np.zeros(16)
+        x[0] = 1
+        x[8] = 1
+        f, p = welch(x, nperseg=8)
+        assert_allclose(f, np.linspace(0, 0.5, 5))
+        q = np.array([0.08333333, 0.15277778, 0.22222222, 0.22222222,
+                      0.11111111])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_real_onesided_odd(self):
+        x = np.zeros(16)
+        x[0] = 1
+        x[8] = 1
+        f, p = welch(x, nperseg=9)
+        assert_allclose(f, np.arange(5.0)/9.0)
+        q = np.array([0.12477455, 0.23430933, 0.17072113, 0.17072113,
+                      0.17072113])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_real_twosided(self):
+        x = np.zeros(16)
+        x[0] = 1
+        x[8] = 1
+        f, p = welch(x, nperseg=8, return_onesided=False)
+        assert_allclose(f, fftfreq(8, 1.0))
+        q = np.array([0.08333333, 0.07638889, 0.11111111, 0.11111111,
+                      0.11111111, 0.11111111, 0.11111111, 0.07638889])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_real_spectrum(self):
+        x = np.zeros(16)
+        x[0] = 1
+        x[8] = 1
+        f, p = welch(x, nperseg=8, scaling='spectrum')
+        assert_allclose(f, np.linspace(0, 0.5, 5))
+        q = np.array([0.015625, 0.02864583, 0.04166667, 0.04166667,
+                      0.02083333])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_integer_onesided_even(self):
+        x = np.zeros(16, dtype=int)
+        x[0] = 1
+        x[8] = 1
+        f, p = welch(x, nperseg=8)
+        assert_allclose(f, np.linspace(0, 0.5, 5))
+        q = np.array([0.08333333, 0.15277778, 0.22222222, 0.22222222,
+                      0.11111111])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_integer_onesided_odd(self):
+        x = np.zeros(16, dtype=int)
+        x[0] = 1
+        x[8] = 1
+        f, p = welch(x, nperseg=9)
+        assert_allclose(f, np.arange(5.0)/9.0)
+        q = np.array([0.12477455, 0.23430933, 0.17072113, 0.17072113,
+                      0.17072113])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_integer_twosided(self):
+        x = np.zeros(16, dtype=int)
+        x[0] = 1
+        x[8] = 1
+        f, p = welch(x, nperseg=8, return_onesided=False)
+        assert_allclose(f, fftfreq(8, 1.0))
+        q = np.array([0.08333333, 0.07638889, 0.11111111, 0.11111111,
+                      0.11111111, 0.11111111, 0.11111111, 0.07638889])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_complex(self):
+        x = np.zeros(16, np.complex128)
+        x[0] = 1.0 + 2.0j
+        x[8] = 1.0 + 2.0j
+        f, p = welch(x, nperseg=8, return_onesided=False)
+        assert_allclose(f, fftfreq(8, 1.0))
+        q = np.array([0.41666667, 0.38194444, 0.55555556, 0.55555556,
+                      0.55555556, 0.55555556, 0.55555556, 0.38194444])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_unk_scaling(self):
+        assert_raises(ValueError, welch, np.zeros(4, np.complex128),
+                      scaling='foo', nperseg=4)
+
+    def test_detrend_linear(self):
+        x = np.arange(10, dtype=np.float64) + 0.04
+        f, p = welch(x, nperseg=10, detrend='linear')
+        assert_allclose(p, np.zeros_like(p), atol=1e-15)
+
+    def test_no_detrending(self):
+        x = np.arange(10, dtype=np.float64) + 0.04
+        f1, p1 = welch(x, nperseg=10, detrend=False)
+        f2, p2 = welch(x, nperseg=10, detrend=lambda x: x)
+        assert_allclose(f1, f2, atol=1e-15)
+        assert_allclose(p1, p2, atol=1e-15)
+
+    def test_detrend_external(self):
+        x = np.arange(10, dtype=np.float64) + 0.04
+        f, p = welch(x, nperseg=10,
+                     detrend=lambda seg: signal.detrend(seg, type='l'))
+        assert_allclose(p, np.zeros_like(p), atol=1e-15)
+
+    def test_detrend_external_nd_m1(self):
+        x = np.arange(40, dtype=np.float64) + 0.04
+        x = x.reshape((2,2,10))
+        f, p = welch(x, nperseg=10,
+                     detrend=lambda seg: signal.detrend(seg, type='l'))
+        assert_allclose(p, np.zeros_like(p), atol=1e-15)
+
+    def test_detrend_external_nd_0(self):
+        x = np.arange(20, dtype=np.float64) + 0.04
+        x = x.reshape((2,1,10))
+        x = np.moveaxis(x, 2, 0)
+        f, p = welch(x, nperseg=10, axis=0,
+                     detrend=lambda seg: signal.detrend(seg, axis=0, type='l'))
+        assert_allclose(p, np.zeros_like(p), atol=1e-15)
+
+    def test_nd_axis_m1(self):
+        x = np.arange(20, dtype=np.float64) + 0.04
+        x = x.reshape((2,1,10))
+        f, p = welch(x, nperseg=10)
+        assert_array_equal(p.shape, (2, 1, 6))
+        assert_allclose(p[0,0,:], p[1,0,:], atol=1e-13, rtol=1e-13)
+        f0, p0 = welch(x[0,0,:], nperseg=10)
+        assert_allclose(p0[np.newaxis,:], p[1,:], atol=1e-13, rtol=1e-13)
+
+    def test_nd_axis_0(self):
+        x = np.arange(20, dtype=np.float64) + 0.04
+        x = x.reshape((10,2,1))
+        f, p = welch(x, nperseg=10, axis=0)
+        assert_array_equal(p.shape, (6,2,1))
+        assert_allclose(p[:,0,0], p[:,1,0], atol=1e-13, rtol=1e-13)
+        f0, p0 = welch(x[:,0,0], nperseg=10)
+        assert_allclose(p0, p[:,1,0], atol=1e-13, rtol=1e-13)
+
+    def test_window_external(self):
+        x = np.zeros(16)
+        x[0] = 1
+        x[8] = 1
+        f, p = welch(x, 10, 'hann', nperseg=8)
+        win = signal.get_window('hann', 8)
+        fe, pe = welch(x, 10, win, nperseg=None)
+        assert_array_almost_equal_nulp(p, pe)
+        assert_array_almost_equal_nulp(f, fe)
+        assert_array_equal(fe.shape, (5,))  # because win length used as nperseg
+        assert_array_equal(pe.shape, (5,))
+        assert_raises(ValueError, welch, x,
+                      10, win, nperseg=4)  # because nperseg != win.shape[-1]
+        win_err = signal.get_window('hann', 32)
+        assert_raises(ValueError, welch, x,
+                      10, win_err, nperseg=None)  # win longer than signal
+
+    def test_empty_input(self):
+        f, p = welch([])
+        assert_array_equal(f.shape, (0,))
+        assert_array_equal(p.shape, (0,))
+        for shape in [(0,), (3,0), (0,5,2)]:
+            f, p = welch(np.empty(shape))
+            assert_array_equal(f.shape, shape)
+            assert_array_equal(p.shape, shape)
+
+    def test_empty_input_other_axis(self):
+        for shape in [(3,0), (0,5,2)]:
+            f, p = welch(np.empty(shape), axis=1)
+            assert_array_equal(f.shape, shape)
+            assert_array_equal(p.shape, shape)
+
+    def test_short_data(self):
+        x = np.zeros(8)
+        x[0] = 1
+        #for string-like window, input signal length < nperseg value gives
+        #UserWarning, sets nperseg to x.shape[-1]
+        with suppress_warnings() as sup:
+            msg = "nperseg = 256 is greater than input length  = 8, using nperseg = 8"
+            sup.filter(UserWarning, msg)
+            f, p = welch(x,window='hann')  # default nperseg
+            f1, p1 = welch(x,window='hann', nperseg=256)  # user-specified nperseg
+        f2, p2 = welch(x, nperseg=8)  # valid nperseg, doesn't give warning
+        assert_allclose(f, f2)
+        assert_allclose(p, p2)
+        assert_allclose(f1, f2)
+        assert_allclose(p1, p2)
+
+    def test_window_long_or_nd(self):
+        assert_raises(ValueError, welch, np.zeros(4), 1, np.array([1,1,1,1,1]))
+        assert_raises(ValueError, welch, np.zeros(4), 1,
+                      np.arange(6).reshape((2,3)))
+
+    def test_nondefault_noverlap(self):
+        x = np.zeros(64)
+        x[::8] = 1
+        f, p = welch(x, nperseg=16, noverlap=4)
+        q = np.array([0, 1./12., 1./3., 1./5., 1./3., 1./5., 1./3., 1./5.,
+                      1./6.])
+        assert_allclose(p, q, atol=1e-12)
+
+    def test_bad_noverlap(self):
+        assert_raises(ValueError, welch, np.zeros(4), 1, 'hann', 2, 7)
+
+    def test_nfft_too_short(self):
+        assert_raises(ValueError, welch, np.ones(12), nfft=3, nperseg=4)
+
+    def test_real_onesided_even_32(self):
+        x = np.zeros(16, 'f')
+        x[0] = 1
+        x[8] = 1
+        f, p = welch(x, nperseg=8)
+        assert_allclose(f, np.linspace(0, 0.5, 5))
+        q = np.array([0.08333333, 0.15277778, 0.22222222, 0.22222222,
+                      0.11111111], 'f')
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+        assert_(p.dtype == q.dtype)
+
+    def test_real_onesided_odd_32(self):
+        x = np.zeros(16, 'f')
+        x[0] = 1
+        x[8] = 1
+        f, p = welch(x, nperseg=9)
+        assert_allclose(f, np.arange(5.0)/9.0)
+        q = np.array([0.12477458, 0.23430935, 0.17072113, 0.17072116,
+                      0.17072113], 'f')
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+        assert_(p.dtype == q.dtype)
+
+    def test_real_twosided_32(self):
+        x = np.zeros(16, 'f')
+        x[0] = 1
+        x[8] = 1
+        f, p = welch(x, nperseg=8, return_onesided=False)
+        assert_allclose(f, fftfreq(8, 1.0))
+        q = np.array([0.08333333, 0.07638889, 0.11111111,
+                      0.11111111, 0.11111111, 0.11111111, 0.11111111,
+                      0.07638889], 'f')
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+        assert_(p.dtype == q.dtype)
+
+    def test_complex_32(self):
+        x = np.zeros(16, 'F')
+        x[0] = 1.0 + 2.0j
+        x[8] = 1.0 + 2.0j
+        f, p = welch(x, nperseg=8, return_onesided=False)
+        assert_allclose(f, fftfreq(8, 1.0))
+        q = np.array([0.41666666, 0.38194442, 0.55555552, 0.55555552,
+                      0.55555558, 0.55555552, 0.55555552, 0.38194442], 'f')
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+        assert_(p.dtype == q.dtype,
+                f'dtype mismatch, {p.dtype}, {q.dtype}')
+
+    def test_padded_freqs(self):
+        x = np.zeros(12)
+
+        nfft = 24
+        f = fftfreq(nfft, 1.0)[:nfft//2+1]
+        f[-1] *= -1
+        fodd, _ = welch(x, nperseg=5, nfft=nfft)
+        feven, _ = welch(x, nperseg=6, nfft=nfft)
+        assert_allclose(f, fodd)
+        assert_allclose(f, feven)
+
+        nfft = 25
+        f = fftfreq(nfft, 1.0)[:(nfft + 1)//2]
+        fodd, _ = welch(x, nperseg=5, nfft=nfft)
+        feven, _ = welch(x, nperseg=6, nfft=nfft)
+        assert_allclose(f, fodd)
+        assert_allclose(f, feven)
+
+    def test_window_correction(self):
+        A = 20
+        fs = 1e4
+        nperseg = int(fs//10)
+        fsig = 300
+        ii = int(fsig*nperseg//fs)  # Freq index of fsig
+
+        tt = np.arange(fs)/fs
+        x = A*np.sin(2*np.pi*fsig*tt)
+
+        for window in ['hann', 'bartlett', ('tukey', 0.1), 'flattop']:
+            _, p_spec = welch(x, fs=fs, nperseg=nperseg, window=window,
+                              scaling='spectrum')
+            freq, p_dens = welch(x, fs=fs, nperseg=nperseg, window=window,
+                                 scaling='density')
+
+            # Check peak height at signal frequency for 'spectrum'
+            assert_allclose(p_spec[ii], A**2/2.0)
+            # Check integrated spectrum RMS for 'density'
+            assert_allclose(np.sqrt(trapezoid(p_dens, freq)), A*np.sqrt(2)/2,
+                            rtol=1e-3)
+
+    def test_axis_rolling(self):
+        np.random.seed(1234)
+
+        x_flat = np.random.randn(1024)
+        _, p_flat = welch(x_flat)
+
+        for a in range(3):
+            newshape = [1,]*3
+            newshape[a] = -1
+            x = x_flat.reshape(newshape)
+
+            _, p_plus = welch(x, axis=a)  # Positive axis index
+            _, p_minus = welch(x, axis=a-x.ndim)  # Negative axis index
+
+            assert_equal(p_flat, p_plus.squeeze(), err_msg=a)
+            assert_equal(p_flat, p_minus.squeeze(), err_msg=a-x.ndim)
+
+    def test_average(self):
+        x = np.zeros(16)
+        x[0] = 1
+        x[8] = 1
+        f, p = welch(x, nperseg=8, average='median')
+        assert_allclose(f, np.linspace(0, 0.5, 5))
+        q = np.array([.1, .05, 0., 1.54074396e-33, 0.])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+        assert_raises(ValueError, welch, x, nperseg=8,
+                      average='unrecognised-average')
+
+
+class TestCSD:
+    def test_pad_shorter_x(self):
+        x = np.zeros(8)
+        y = np.zeros(12)
+
+        f = np.linspace(0, 0.5, 7)
+        c = np.zeros(7,dtype=np.complex128)
+        f1, c1 = csd(x, y, nperseg=12)
+
+        assert_allclose(f, f1)
+        assert_allclose(c, c1)
+
+    def test_pad_shorter_y(self):
+        x = np.zeros(12)
+        y = np.zeros(8)
+
+        f = np.linspace(0, 0.5, 7)
+        c = np.zeros(7,dtype=np.complex128)
+        f1, c1 = csd(x, y, nperseg=12)
+
+        assert_allclose(f, f1)
+        assert_allclose(c, c1)
+
+    def test_real_onesided_even(self):
+        x = np.zeros(16)
+        x[0] = 1
+        x[8] = 1
+        f, p = csd(x, x, nperseg=8)
+        assert_allclose(f, np.linspace(0, 0.5, 5))
+        q = np.array([0.08333333, 0.15277778, 0.22222222, 0.22222222,
+                      0.11111111])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_real_onesided_odd(self):
+        x = np.zeros(16)
+        x[0] = 1
+        x[8] = 1
+        f, p = csd(x, x, nperseg=9)
+        assert_allclose(f, np.arange(5.0)/9.0)
+        q = np.array([0.12477455, 0.23430933, 0.17072113, 0.17072113,
+                      0.17072113])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_real_twosided(self):
+        x = np.zeros(16)
+        x[0] = 1
+        x[8] = 1
+        f, p = csd(x, x, nperseg=8, return_onesided=False)
+        assert_allclose(f, fftfreq(8, 1.0))
+        q = np.array([0.08333333, 0.07638889, 0.11111111, 0.11111111,
+                      0.11111111, 0.11111111, 0.11111111, 0.07638889])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_real_spectrum(self):
+        x = np.zeros(16)
+        x[0] = 1
+        x[8] = 1
+        f, p = csd(x, x, nperseg=8, scaling='spectrum')
+        assert_allclose(f, np.linspace(0, 0.5, 5))
+        q = np.array([0.015625, 0.02864583, 0.04166667, 0.04166667,
+                      0.02083333])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_integer_onesided_even(self):
+        x = np.zeros(16, dtype=int)
+        x[0] = 1
+        x[8] = 1
+        f, p = csd(x, x, nperseg=8)
+        assert_allclose(f, np.linspace(0, 0.5, 5))
+        q = np.array([0.08333333, 0.15277778, 0.22222222, 0.22222222,
+                      0.11111111])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_integer_onesided_odd(self):
+        x = np.zeros(16, dtype=int)
+        x[0] = 1
+        x[8] = 1
+        f, p = csd(x, x, nperseg=9)
+        assert_allclose(f, np.arange(5.0)/9.0)
+        q = np.array([0.12477455, 0.23430933, 0.17072113, 0.17072113,
+                      0.17072113])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_integer_twosided(self):
+        x = np.zeros(16, dtype=int)
+        x[0] = 1
+        x[8] = 1
+        f, p = csd(x, x, nperseg=8, return_onesided=False)
+        assert_allclose(f, fftfreq(8, 1.0))
+        q = np.array([0.08333333, 0.07638889, 0.11111111, 0.11111111,
+                      0.11111111, 0.11111111, 0.11111111, 0.07638889])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_complex(self):
+        x = np.zeros(16, np.complex128)
+        x[0] = 1.0 + 2.0j
+        x[8] = 1.0 + 2.0j
+        f, p = csd(x, x, nperseg=8, return_onesided=False)
+        assert_allclose(f, fftfreq(8, 1.0))
+        q = np.array([0.41666667, 0.38194444, 0.55555556, 0.55555556,
+                      0.55555556, 0.55555556, 0.55555556, 0.38194444])
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+
+    def test_unk_scaling(self):
+        assert_raises(ValueError, csd, np.zeros(4, np.complex128),
+                      np.ones(4, np.complex128), scaling='foo', nperseg=4)
+
+    def test_detrend_linear(self):
+        x = np.arange(10, dtype=np.float64) + 0.04
+        f, p = csd(x, x, nperseg=10, detrend='linear')
+        assert_allclose(p, np.zeros_like(p), atol=1e-15)
+
+    def test_no_detrending(self):
+        x = np.arange(10, dtype=np.float64) + 0.04
+        f1, p1 = csd(x, x, nperseg=10, detrend=False)
+        f2, p2 = csd(x, x, nperseg=10, detrend=lambda x: x)
+        assert_allclose(f1, f2, atol=1e-15)
+        assert_allclose(p1, p2, atol=1e-15)
+
+    def test_detrend_external(self):
+        x = np.arange(10, dtype=np.float64) + 0.04
+        f, p = csd(x, x, nperseg=10,
+                   detrend=lambda seg: signal.detrend(seg, type='l'))
+        assert_allclose(p, np.zeros_like(p), atol=1e-15)
+
+    def test_detrend_external_nd_m1(self):
+        x = np.arange(40, dtype=np.float64) + 0.04
+        x = x.reshape((2,2,10))
+        f, p = csd(x, x, nperseg=10,
+                   detrend=lambda seg: signal.detrend(seg, type='l'))
+        assert_allclose(p, np.zeros_like(p), atol=1e-15)
+
+    def test_detrend_external_nd_0(self):
+        x = np.arange(20, dtype=np.float64) + 0.04
+        x = x.reshape((2,1,10))
+        x = np.moveaxis(x, 2, 0)
+        f, p = csd(x, x, nperseg=10, axis=0,
+                   detrend=lambda seg: signal.detrend(seg, axis=0, type='l'))
+        assert_allclose(p, np.zeros_like(p), atol=1e-15)
+
+    def test_nd_axis_m1(self):
+        x = np.arange(20, dtype=np.float64) + 0.04
+        x = x.reshape((2,1,10))
+        f, p = csd(x, x, nperseg=10)
+        assert_array_equal(p.shape, (2, 1, 6))
+        assert_allclose(p[0,0,:], p[1,0,:], atol=1e-13, rtol=1e-13)
+        f0, p0 = csd(x[0,0,:], x[0,0,:], nperseg=10)
+        assert_allclose(p0[np.newaxis,:], p[1,:], atol=1e-13, rtol=1e-13)
+
+    def test_nd_axis_0(self):
+        x = np.arange(20, dtype=np.float64) + 0.04
+        x = x.reshape((10,2,1))
+        f, p = csd(x, x, nperseg=10, axis=0)
+        assert_array_equal(p.shape, (6,2,1))
+        assert_allclose(p[:,0,0], p[:,1,0], atol=1e-13, rtol=1e-13)
+        f0, p0 = csd(x[:,0,0], x[:,0,0], nperseg=10)
+        assert_allclose(p0, p[:,1,0], atol=1e-13, rtol=1e-13)
+
+    def test_window_external(self):
+        x = np.zeros(16)
+        x[0] = 1
+        x[8] = 1
+        f, p = csd(x, x, 10, 'hann', 8)
+        win = signal.get_window('hann', 8)
+        fe, pe = csd(x, x, 10, win, nperseg=None)
+        assert_array_almost_equal_nulp(p, pe)
+        assert_array_almost_equal_nulp(f, fe)
+        assert_array_equal(fe.shape, (5,))  # because win length used as nperseg
+        assert_array_equal(pe.shape, (5,))
+        assert_raises(ValueError, csd, x, x,
+                      10, win, nperseg=256)  # because nperseg != win.shape[-1]
+        win_err = signal.get_window('hann', 32)
+        assert_raises(ValueError, csd, x, x,
+              10, win_err, nperseg=None)  # because win longer than signal
+
+    def test_empty_input(self):
+        f, p = csd([],np.zeros(10))
+        assert_array_equal(f.shape, (0,))
+        assert_array_equal(p.shape, (0,))
+
+        f, p = csd(np.zeros(10),[])
+        assert_array_equal(f.shape, (0,))
+        assert_array_equal(p.shape, (0,))
+
+        for shape in [(0,), (3,0), (0,5,2)]:
+            f, p = csd(np.empty(shape), np.empty(shape))
+            assert_array_equal(f.shape, shape)
+            assert_array_equal(p.shape, shape)
+
+        f, p = csd(np.ones(10), np.empty((5,0)))
+        assert_array_equal(f.shape, (5,0))
+        assert_array_equal(p.shape, (5,0))
+
+        f, p = csd(np.empty((5,0)), np.ones(10))
+        assert_array_equal(f.shape, (5,0))
+        assert_array_equal(p.shape, (5,0))
+
+    def test_empty_input_other_axis(self):
+        for shape in [(3,0), (0,5,2)]:
+            f, p = csd(np.empty(shape), np.empty(shape), axis=1)
+            assert_array_equal(f.shape, shape)
+            assert_array_equal(p.shape, shape)
+
+        f, p = csd(np.empty((10,10,3)), np.zeros((10,0,1)), axis=1)
+        assert_array_equal(f.shape, (10,0,3))
+        assert_array_equal(p.shape, (10,0,3))
+
+        f, p = csd(np.empty((10,0,1)), np.zeros((10,10,3)), axis=1)
+        assert_array_equal(f.shape, (10,0,3))
+        assert_array_equal(p.shape, (10,0,3))
+
+    def test_short_data(self):
+        x = np.zeros(8)
+        x[0] = 1
+
+        #for string-like window, input signal length < nperseg value gives
+        #UserWarning, sets nperseg to x.shape[-1]
+        with suppress_warnings() as sup:
+            msg = "nperseg = 256 is greater than input length  = 8, using nperseg = 8"
+            sup.filter(UserWarning, msg)
+            f, p = csd(x, x, window='hann')  # default nperseg
+            f1, p1 = csd(x, x, window='hann', nperseg=256)  # user-specified nperseg
+        f2, p2 = csd(x, x, nperseg=8)  # valid nperseg, doesn't give warning
+        assert_allclose(f, f2)
+        assert_allclose(p, p2)
+        assert_allclose(f1, f2)
+        assert_allclose(p1, p2)
+
+    def test_window_long_or_nd(self):
+        assert_raises(ValueError, csd, np.zeros(4), np.ones(4), 1,
+                      np.array([1,1,1,1,1]))
+        assert_raises(ValueError, csd, np.zeros(4), np.ones(4), 1,
+                      np.arange(6).reshape((2,3)))
+
+    def test_nondefault_noverlap(self):
+        x = np.zeros(64)
+        x[::8] = 1
+        f, p = csd(x, x, nperseg=16, noverlap=4)
+        q = np.array([0, 1./12., 1./3., 1./5., 1./3., 1./5., 1./3., 1./5.,
+                      1./6.])
+        assert_allclose(p, q, atol=1e-12)
+
+    def test_bad_noverlap(self):
+        assert_raises(ValueError, csd, np.zeros(4), np.ones(4), 1, 'hann',
+                      2, 7)
+
+    def test_nfft_too_short(self):
+        assert_raises(ValueError, csd, np.ones(12), np.zeros(12), nfft=3,
+                      nperseg=4)
+
+    def test_real_onesided_even_32(self):
+        x = np.zeros(16, 'f')
+        x[0] = 1
+        x[8] = 1
+        f, p = csd(x, x, nperseg=8)
+        assert_allclose(f, np.linspace(0, 0.5, 5))
+        q = np.array([0.08333333, 0.15277778, 0.22222222, 0.22222222,
+                      0.11111111], 'f')
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+        assert_(p.dtype == q.dtype)
+
+    def test_real_onesided_odd_32(self):
+        x = np.zeros(16, 'f')
+        x[0] = 1
+        x[8] = 1
+        f, p = csd(x, x, nperseg=9)
+        assert_allclose(f, np.arange(5.0)/9.0)
+        q = np.array([0.12477458, 0.23430935, 0.17072113, 0.17072116,
+                      0.17072113], 'f')
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+        assert_(p.dtype == q.dtype)
+
+    def test_real_twosided_32(self):
+        x = np.zeros(16, 'f')
+        x[0] = 1
+        x[8] = 1
+        f, p = csd(x, x, nperseg=8, return_onesided=False)
+        assert_allclose(f, fftfreq(8, 1.0))
+        q = np.array([0.08333333, 0.07638889, 0.11111111,
+                      0.11111111, 0.11111111, 0.11111111, 0.11111111,
+                      0.07638889], 'f')
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+        assert_(p.dtype == q.dtype)
+
+    def test_complex_32(self):
+        x = np.zeros(16, 'F')
+        x[0] = 1.0 + 2.0j
+        x[8] = 1.0 + 2.0j
+        f, p = csd(x, x, nperseg=8, return_onesided=False)
+        assert_allclose(f, fftfreq(8, 1.0))
+        q = np.array([0.41666666, 0.38194442, 0.55555552, 0.55555552,
+                      0.55555558, 0.55555552, 0.55555552, 0.38194442], 'f')
+        assert_allclose(p, q, atol=1e-7, rtol=1e-7)
+        assert_(p.dtype == q.dtype,
+                f'dtype mismatch, {p.dtype}, {q.dtype}')
+
+    def test_padded_freqs(self):
+        x = np.zeros(12)
+        y = np.ones(12)
+
+        nfft = 24
+        f = fftfreq(nfft, 1.0)[:nfft//2+1]
+        f[-1] *= -1
+        fodd, _ = csd(x, y, nperseg=5, nfft=nfft)
+        feven, _ = csd(x, y, nperseg=6, nfft=nfft)
+        assert_allclose(f, fodd)
+        assert_allclose(f, feven)
+
+        nfft = 25
+        f = fftfreq(nfft, 1.0)[:(nfft + 1)//2]
+        fodd, _ = csd(x, y, nperseg=5, nfft=nfft)
+        feven, _ = csd(x, y, nperseg=6, nfft=nfft)
+        assert_allclose(f, fodd)
+        assert_allclose(f, feven)
+
+    def test_copied_data(self):
+        x = np.random.randn(64)
+        y = x.copy()
+
+        _, p_same = csd(x, x, nperseg=8, average='mean',
+                        return_onesided=False)
+        _, p_copied = csd(x, y, nperseg=8, average='mean',
+                          return_onesided=False)
+        assert_allclose(p_same, p_copied)
+
+        _, p_same = csd(x, x, nperseg=8, average='median',
+                        return_onesided=False)
+        _, p_copied = csd(x, y, nperseg=8, average='median',
+                          return_onesided=False)
+        assert_allclose(p_same, p_copied)
+
+
+class TestCoherence:
+    def test_identical_input(self):
+        x = np.random.randn(20)
+        y = np.copy(x)  # So `y is x` -> False
+
+        f = np.linspace(0, 0.5, 6)
+        C = np.ones(6)
+        f1, C1 = coherence(x, y, nperseg=10)
+
+        assert_allclose(f, f1)
+        assert_allclose(C, C1)
+
+    def test_phase_shifted_input(self):
+        x = np.random.randn(20)
+        y = -x
+
+        f = np.linspace(0, 0.5, 6)
+        C = np.ones(6)
+        f1, C1 = coherence(x, y, nperseg=10)
+
+        assert_allclose(f, f1)
+        assert_allclose(C, C1)
+
+
+class TestSpectrogram:
+    def test_average_all_segments(self):
+        x = np.random.randn(1024)
+
+        fs = 1.0
+        window = ('tukey', 0.25)
+        nperseg = 16
+        noverlap = 2
+
+        f, _, P = spectrogram(x, fs, window, nperseg, noverlap)
+        fw, Pw = welch(x, fs, window, nperseg, noverlap)
+        assert_allclose(f, fw)
+        assert_allclose(np.mean(P, axis=-1), Pw)
+
+    def test_window_external(self):
+        x = np.random.randn(1024)
+
+        fs = 1.0
+        window = ('tukey', 0.25)
+        nperseg = 16
+        noverlap = 2
+        f, _, P = spectrogram(x, fs, window, nperseg, noverlap)
+
+        win = signal.get_window(('tukey', 0.25), 16)
+        fe, _, Pe = spectrogram(x, fs, win, nperseg=None, noverlap=2)
+        assert_array_equal(fe.shape, (9,))  # because win length used as nperseg
+        assert_array_equal(Pe.shape, (9,73))
+        assert_raises(ValueError, spectrogram, x,
+                      fs, win, nperseg=8)  # because nperseg != win.shape[-1]
+        win_err = signal.get_window(('tukey', 0.25), 2048)
+        assert_raises(ValueError, spectrogram, x,
+                      fs, win_err, nperseg=None)  # win longer than signal
+
+    def test_short_data(self):
+        x = np.random.randn(1024)
+        fs = 1.0
+
+        #for string-like window, input signal length < nperseg value gives
+        #UserWarning, sets nperseg to x.shape[-1]
+        f, _, p = spectrogram(x, fs, window=('tukey',0.25))  # default nperseg
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning,
+                       "nperseg = 1025 is greater than input length  = 1024, "
+                       "using nperseg = 1024",)
+            f1, _, p1 = spectrogram(x, fs, window=('tukey',0.25),
+                                    nperseg=1025)  # user-specified nperseg
+        f2, _, p2 = spectrogram(x, fs, nperseg=256)  # to compare w/default
+        f3, _, p3 = spectrogram(x, fs, nperseg=1024)  # compare w/user-spec'd
+        assert_allclose(f, f2)
+        assert_allclose(p, p2)
+        assert_allclose(f1, f3)
+        assert_allclose(p1, p3)
+
+class TestLombscargle:
+    def test_frequency(self):
+        """Test if frequency location of peak corresponds to frequency of
+        generated input signal.
+        """
+
+        # Input parameters
+        ampl = 2.
+        w = 1.
+        phi = 0.5 * np.pi
+        nin = 100
+        nout = 1000
+        p = 0.7  # Fraction of points to select
+
+        # Randomly select a fraction of an array with timesteps
+        np.random.seed(2353425)
+        r = np.random.rand(nin)
+        t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
+
+        # Plot a sine wave for the selected times
+        x = ampl * np.sin(w*t + phi)
+
+        # Define the array of frequencies for which to compute the periodogram
+        f = np.linspace(0.01, 10., nout)
+
+        # Calculate Lomb-Scargle periodogram
+        P = lombscargle(t, x, f)
+
+        # Check if difference between found frequency maximum and input
+        # frequency is less than accuracy
+        delta = f[1] - f[0]
+        assert_(w - f[np.argmax(P)] < (delta/2.))
+
+    def test_amplitude(self):
+        # Test if height of peak in normalized Lomb-Scargle periodogram
+        # corresponds to amplitude of the generated input signal.
+
+        # Input parameters
+        ampl = 2.
+        w = 1.
+        phi = 0.5 * np.pi
+        nin = 100
+        nout = 1000
+        p = 0.7  # Fraction of points to select
+
+        # Randomly select a fraction of an array with timesteps
+        np.random.seed(2353425)
+        r = np.random.rand(nin)
+        t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
+
+        # Plot a sine wave for the selected times
+        x = ampl * np.sin(w*t + phi)
+
+        # Define the array of frequencies for which to compute the periodogram
+        f = np.linspace(0.01, 10., nout)
+
+        # Calculate Lomb-Scargle periodogram
+        pgram = lombscargle(t, x, f)
+
+        # Normalize
+        pgram = np.sqrt(4 * pgram / t.shape[0])
+
+        # Check if difference between found frequency maximum and input
+        # frequency is less than accuracy
+        assert_approx_equal(np.max(pgram), ampl, significant=2)
+
+    def test_precenter(self):
+        # Test if precenter gives the same result as manually precentering.
+
+        # Input parameters
+        ampl = 2.
+        w = 1.
+        phi = 0.5 * np.pi
+        nin = 100
+        nout = 1000
+        p = 0.7  # Fraction of points to select
+        offset = 0.15  # Offset to be subtracted in pre-centering
+
+        # Randomly select a fraction of an array with timesteps
+        np.random.seed(2353425)
+        r = np.random.rand(nin)
+        t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
+
+        # Plot a sine wave for the selected times
+        x = ampl * np.sin(w*t + phi) + offset
+
+        # Define the array of frequencies for which to compute the periodogram
+        f = np.linspace(0.01, 10., nout)
+
+        # Calculate Lomb-Scargle periodogram
+        pgram = lombscargle(t, x, f, precenter=True)
+        pgram2 = lombscargle(t, x - x.mean(), f, precenter=False)
+
+        # check if centering worked
+        assert_allclose(pgram, pgram2)
+
+    def test_normalize(self):
+        # Test normalize option of Lomb-Scarge.
+
+        # Input parameters
+        ampl = 2.
+        w = 1.
+        phi = 0.5 * np.pi
+        nin = 100
+        nout = 1000
+        p = 0.7  # Fraction of points to select
+
+        # Randomly select a fraction of an array with timesteps
+        np.random.seed(2353425)
+        r = np.random.rand(nin)
+        t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
+
+        # Plot a sine wave for the selected times
+        x = ampl * np.sin(w*t + phi)
+
+        # Define the array of frequencies for which to compute the periodogram
+        f = np.linspace(0.01, 10., nout)
+
+        # Calculate Lomb-Scargle periodogram
+        pgram = lombscargle(t, x, f)
+        pgram2 = lombscargle(t, x, f, normalize=True)
+
+        # check if normalization works as expected
+        assert_allclose(pgram * 2 / np.dot(x, x), pgram2)
+        assert_approx_equal(np.max(pgram2), 1.0, significant=2)
+
+    def test_wrong_shape(self):
+        t = np.linspace(0, 1, 1)
+        x = np.linspace(0, 1, 2)
+        f = np.linspace(0, 1, 3)
+        assert_raises(ValueError, lombscargle, t, x, f)
+
+    def test_zero_division(self):
+        t = np.zeros(1)
+        x = np.zeros(1)
+        f = np.zeros(1)
+        assert_raises(ZeroDivisionError, lombscargle, t, x, f)
+
+    def test_lombscargle_atan_vs_atan2(self):
+        # https://github.com/scipy/scipy/issues/3787
+        # This raised a ZeroDivisionError.
+        t = np.linspace(0, 10, 1000, endpoint=False)
+        x = np.sin(4*t)
+        f = np.linspace(0, 50, 500, endpoint=False) + 0.1
+        lombscargle(t, x, f*2*np.pi)
+
+
+class TestSTFT:
+    def test_input_validation(self):
+
+        def chk_VE(match):
+            """Assert for a ValueError matching regexp `match`.
+
+            This little wrapper allows a more concise code layout.
+            """
+            return pytest.raises(ValueError, match=match)
+
+        # Checks for check_COLA():
+        with chk_VE('nperseg must be a positive integer'):
+            check_COLA('hann', -10, 0)
+        with chk_VE('noverlap must be less than nperseg.'):
+            check_COLA('hann', 10, 20)
+        with chk_VE('window must be 1-D'):
+            check_COLA(np.ones((2, 2)), 10, 0)
+        with chk_VE('window must have length of nperseg'):
+            check_COLA(np.ones(20), 10, 0)
+
+        # Checks for check_NOLA():
+        with chk_VE('nperseg must be a positive integer'):
+            check_NOLA('hann', -10, 0)
+        with chk_VE('noverlap must be less than nperseg'):
+            check_NOLA('hann', 10, 20)
+        with chk_VE('window must be 1-D'):
+            check_NOLA(np.ones((2, 2)), 10, 0)
+        with chk_VE('window must have length of nperseg'):
+            check_NOLA(np.ones(20), 10, 0)
+        with chk_VE('noverlap must be a nonnegative integer'):
+            check_NOLA('hann', 64, -32)
+
+        x = np.zeros(1024)
+        z = stft(x)[2]
+
+        # Checks for stft():
+        with chk_VE('window must be 1-D'):
+            stft(x, window=np.ones((2, 2)))
+        with chk_VE('value specified for nperseg is different ' +
+                    'from length of window'):
+            stft(x, window=np.ones(10), nperseg=256)
+        with chk_VE('nperseg must be a positive integer'):
+            stft(x, nperseg=-256)
+        with chk_VE('noverlap must be less than nperseg.'):
+            stft(x, nperseg=256, noverlap=1024)
+        with chk_VE('nfft must be greater than or equal to nperseg.'):
+            stft(x, nperseg=256, nfft=8)
+
+        # Checks for istft():
+        with chk_VE('Input stft must be at least 2d!'):
+            istft(x)
+        with chk_VE('window must be 1-D'):
+            istft(z, window=np.ones((2, 2)))
+        with chk_VE('window must have length of 256'):
+            istft(z, window=np.ones(10), nperseg=256)
+        with chk_VE('nperseg must be a positive integer'):
+            istft(z, nperseg=-256)
+        with chk_VE('noverlap must be less than nperseg.'):
+            istft(z, nperseg=256, noverlap=1024)
+        with chk_VE('nfft must be greater than or equal to nperseg.'):
+            istft(z, nperseg=256, nfft=8)
+        with pytest.warns(UserWarning, match="NOLA condition failed, " +
+                          "STFT may not be invertible"):
+            istft(z, nperseg=256, noverlap=0, window='hann')
+        with chk_VE('Must specify differing time and frequency axes!'):
+            istft(z, time_axis=0, freq_axis=0)
+
+        # Checks for _spectral_helper():
+        with chk_VE("Unknown value for mode foo, must be one of: " +
+                    r"\{'psd', 'stft'\}"):
+            _spectral_helper(x, x, mode='foo')
+        with chk_VE("x and y must be equal if mode is 'stft'"):
+            _spectral_helper(x[:512], x[512:], mode='stft')
+        with chk_VE("Unknown boundary option 'foo', must be one of: " +
+                    r"\['even', 'odd', 'constant', 'zeros', None\]"):
+            _spectral_helper(x, x, boundary='foo')
+
+        scaling = "not_valid"
+        with chk_VE(fr"Parameter {scaling=} not in \['spectrum', 'psd'\]!"):
+            stft(x, scaling=scaling)
+        with chk_VE(fr"Parameter {scaling=} not in \['spectrum', 'psd'\]!"):
+            istft(z, scaling=scaling)
+
+    def test_check_COLA(self):
+        settings = [
+                    ('boxcar', 10, 0),
+                    ('boxcar', 10, 9),
+                    ('bartlett', 51, 26),
+                    ('hann', 256, 128),
+                    ('hann', 256, 192),
+                    ('blackman', 300, 200),
+                    (('tukey', 0.5), 256, 64),
+                    ('hann', 256, 255),
+                    ]
+
+        for setting in settings:
+            msg = '{}, {}, {}'.format(*setting)
+            assert_equal(True, check_COLA(*setting), err_msg=msg)
+
+    def test_check_NOLA(self):
+        settings_pass = [
+                    ('boxcar', 10, 0),
+                    ('boxcar', 10, 9),
+                    ('boxcar', 10, 7),
+                    ('bartlett', 51, 26),
+                    ('bartlett', 51, 10),
+                    ('hann', 256, 128),
+                    ('hann', 256, 192),
+                    ('hann', 256, 37),
+                    ('blackman', 300, 200),
+                    ('blackman', 300, 123),
+                    (('tukey', 0.5), 256, 64),
+                    (('tukey', 0.5), 256, 38),
+                    ('hann', 256, 255),
+                    ('hann', 256, 39),
+                    ]
+        for setting in settings_pass:
+            msg = '{}, {}, {}'.format(*setting)
+            assert_equal(True, check_NOLA(*setting), err_msg=msg)
+
+        w_fail = np.ones(16)
+        w_fail[::2] = 0
+        settings_fail = [
+                    (w_fail, len(w_fail), len(w_fail) // 2),
+                    ('hann', 64, 0),
+        ]
+        for setting in settings_fail:
+            msg = '{}, {}, {}'.format(*setting)
+            assert_equal(False, check_NOLA(*setting), err_msg=msg)
+
+    def test_average_all_segments(self):
+        np.random.seed(1234)
+        x = np.random.randn(1024)
+
+        fs = 1.0
+        window = 'hann'
+        nperseg = 16
+        noverlap = 8
+
+        # Compare twosided, because onesided welch doubles non-DC terms to
+        # account for power at negative frequencies. stft doesn't do this,
+        # because it breaks invertibility.
+        f, _, Z = stft(x, fs, window, nperseg, noverlap, padded=False,
+                       return_onesided=False, boundary=None)
+        fw, Pw = welch(x, fs, window, nperseg, noverlap, return_onesided=False,
+                       scaling='spectrum', detrend=False)
+
+        assert_allclose(f, fw)
+        assert_allclose(np.mean(np.abs(Z)**2, axis=-1), Pw)
+
+    def test_permute_axes(self):
+        np.random.seed(1234)
+        x = np.random.randn(1024)
+
+        fs = 1.0
+        window = 'hann'
+        nperseg = 16
+        noverlap = 8
+
+        f1, t1, Z1 = stft(x, fs, window, nperseg, noverlap)
+        f2, t2, Z2 = stft(x.reshape((-1, 1, 1)), fs, window, nperseg, noverlap,
+                          axis=0)
+
+        t3, x1 = istft(Z1, fs, window, nperseg, noverlap)
+        t4, x2 = istft(Z2.T, fs, window, nperseg, noverlap, time_axis=0,
+                       freq_axis=-1)
+
+        assert_allclose(f1, f2)
+        assert_allclose(t1, t2)
+        assert_allclose(t3, t4)
+        assert_allclose(Z1, Z2[:, 0, 0, :])
+        assert_allclose(x1, x2[:, 0, 0])
+
+    @pytest.mark.parametrize('scaling', ['spectrum', 'psd'])
+    def test_roundtrip_real(self, scaling):
+        np.random.seed(1234)
+
+        settings = [
+                    ('boxcar', 100, 10, 0),           # Test no overlap
+                    ('boxcar', 100, 10, 9),           # Test high overlap
+                    ('bartlett', 101, 51, 26),        # Test odd nperseg
+                    ('hann', 1024, 256, 128),         # Test defaults
+                    (('tukey', 0.5), 1152, 256, 64),  # Test Tukey
+                    ('hann', 1024, 256, 255),         # Test overlapped hann
+                    ]
+
+        for window, N, nperseg, noverlap in settings:
+            t = np.arange(N)
+            x = 10*np.random.randn(t.size)
+
+            _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
+                            window=window, detrend=None, padded=False,
+                            scaling=scaling)
+
+            tr, xr = istft(zz, nperseg=nperseg, noverlap=noverlap,
+                           window=window, scaling=scaling)
+
+            msg = f'{window}, {noverlap}'
+            assert_allclose(t, tr, err_msg=msg)
+            assert_allclose(x, xr, err_msg=msg)
+
+    def test_roundtrip_not_nola(self):
+        np.random.seed(1234)
+
+        w_fail = np.ones(16)
+        w_fail[::2] = 0
+        settings = [
+                    (w_fail, 256, len(w_fail), len(w_fail) // 2),
+                    ('hann', 256, 64, 0),
+        ]
+
+        for window, N, nperseg, noverlap in settings:
+            msg = f'{window}, {N}, {nperseg}, {noverlap}'
+            assert not check_NOLA(window, nperseg, noverlap), msg
+
+            t = np.arange(N)
+            x = 10 * np.random.randn(t.size)
+
+            _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
+                            window=window, detrend=None, padded=True,
+                            boundary='zeros')
+            with pytest.warns(UserWarning, match='NOLA'):
+                tr, xr = istft(zz, nperseg=nperseg, noverlap=noverlap,
+                               window=window, boundary=True)
+
+            assert np.allclose(t, tr[:len(t)]), msg
+            assert not np.allclose(x, xr[:len(x)]), msg
+
+    def test_roundtrip_nola_not_cola(self):
+        np.random.seed(1234)
+
+        settings = [
+                    ('boxcar', 100, 10, 3),           # NOLA True, COLA False
+                    ('bartlett', 101, 51, 37),        # NOLA True, COLA False
+                    ('hann', 1024, 256, 127),         # NOLA True, COLA False
+                    (('tukey', 0.5), 1152, 256, 14),  # NOLA True, COLA False
+                    ('hann', 1024, 256, 5),           # NOLA True, COLA False
+                    ]
+
+        for window, N, nperseg, noverlap in settings:
+            msg = f'{window}, {nperseg}, {noverlap}'
+            assert check_NOLA(window, nperseg, noverlap), msg
+            assert not check_COLA(window, nperseg, noverlap), msg
+
+            t = np.arange(N)
+            x = 10 * np.random.randn(t.size)
+
+            _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
+                            window=window, detrend=None, padded=True,
+                            boundary='zeros')
+
+            tr, xr = istft(zz, nperseg=nperseg, noverlap=noverlap,
+                           window=window, boundary=True)
+
+            msg = f'{window}, {noverlap}'
+            assert_allclose(t, tr[:len(t)], err_msg=msg)
+            assert_allclose(x, xr[:len(x)], err_msg=msg)
+
+    def test_roundtrip_float32(self):
+        np.random.seed(1234)
+
+        settings = [('hann', 1024, 256, 128)]
+
+        for window, N, nperseg, noverlap in settings:
+            t = np.arange(N)
+            x = 10*np.random.randn(t.size)
+            x = x.astype(np.float32)
+
+            _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
+                            window=window, detrend=None, padded=False)
+
+            tr, xr = istft(zz, nperseg=nperseg, noverlap=noverlap,
+                           window=window)
+
+            msg = f'{window}, {noverlap}'
+            assert_allclose(t, t, err_msg=msg)
+            assert_allclose(x, xr, err_msg=msg, rtol=1e-4, atol=1e-5)
+            assert_(x.dtype == xr.dtype)
+
+    @pytest.mark.parametrize('scaling', ['spectrum', 'psd'])
+    def test_roundtrip_complex(self, scaling):
+        np.random.seed(1234)
+
+        settings = [
+                    ('boxcar', 100, 10, 0),           # Test no overlap
+                    ('boxcar', 100, 10, 9),           # Test high overlap
+                    ('bartlett', 101, 51, 26),        # Test odd nperseg
+                    ('hann', 1024, 256, 128),         # Test defaults
+                    (('tukey', 0.5), 1152, 256, 64),  # Test Tukey
+                    ('hann', 1024, 256, 255),         # Test overlapped hann
+                    ]
+
+        for window, N, nperseg, noverlap in settings:
+            t = np.arange(N)
+            x = 10*np.random.randn(t.size) + 10j*np.random.randn(t.size)
+
+            _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
+                            window=window, detrend=None, padded=False,
+                            return_onesided=False, scaling=scaling)
+
+            tr, xr = istft(zz, nperseg=nperseg, noverlap=noverlap,
+                           window=window, input_onesided=False,
+                           scaling=scaling)
+
+            msg = f'{window}, {nperseg}, {noverlap}'
+            assert_allclose(t, tr, err_msg=msg)
+            assert_allclose(x, xr, err_msg=msg)
+
+        # Check that asking for onesided switches to twosided
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning,
+                       "Input data is complex, switching to return_onesided=False")
+            _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
+                            window=window, detrend=None, padded=False,
+                            return_onesided=True, scaling=scaling)
+
+        tr, xr = istft(zz, nperseg=nperseg, noverlap=noverlap,
+                       window=window, input_onesided=False, scaling=scaling)
+
+        msg = f'{window}, {nperseg}, {noverlap}'
+        assert_allclose(t, tr, err_msg=msg)
+        assert_allclose(x, xr, err_msg=msg)
+
+    def test_roundtrip_boundary_extension(self):
+        np.random.seed(1234)
+
+        # Test against boxcar, since window is all ones, and thus can be fully
+        # recovered with no boundary extension
+
+        settings = [
+                    ('boxcar', 100, 10, 0),           # Test no overlap
+                    ('boxcar', 100, 10, 9),           # Test high overlap
+                    ]
+
+        for window, N, nperseg, noverlap in settings:
+            t = np.arange(N)
+            x = 10*np.random.randn(t.size)
+
+            _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
+                           window=window, detrend=None, padded=True,
+                           boundary=None)
+
+            _, xr = istft(zz, noverlap=noverlap, window=window, boundary=False)
+
+            for boundary in ['even', 'odd', 'constant', 'zeros']:
+                _, _, zz_ext = stft(x, nperseg=nperseg, noverlap=noverlap,
+                                window=window, detrend=None, padded=True,
+                                boundary=boundary)
+
+                _, xr_ext = istft(zz_ext, noverlap=noverlap, window=window,
+                                boundary=True)
+
+                msg = f'{window}, {noverlap}, {boundary}'
+                assert_allclose(x, xr, err_msg=msg)
+                assert_allclose(x, xr_ext, err_msg=msg)
+
+    def test_roundtrip_padded_signal(self):
+        np.random.seed(1234)
+
+        settings = [
+                    ('boxcar', 101, 10, 0),
+                    ('hann', 1000, 256, 128),
+                    ]
+
+        for window, N, nperseg, noverlap in settings:
+            t = np.arange(N)
+            x = 10*np.random.randn(t.size)
+
+            _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
+                            window=window, detrend=None, padded=True)
+
+            tr, xr = istft(zz, noverlap=noverlap, window=window)
+
+            msg = f'{window}, {noverlap}'
+            # Account for possible zero-padding at the end
+            assert_allclose(t, tr[:t.size], err_msg=msg)
+            assert_allclose(x, xr[:x.size], err_msg=msg)
+
+    def test_roundtrip_padded_FFT(self):
+        np.random.seed(1234)
+
+        settings = [
+                    ('hann', 1024, 256, 128, 512),
+                    ('hann', 1024, 256, 128, 501),
+                    ('boxcar', 100, 10, 0, 33),
+                    (('tukey', 0.5), 1152, 256, 64, 1024),
+                    ]
+
+        for window, N, nperseg, noverlap, nfft in settings:
+            t = np.arange(N)
+            x = 10*np.random.randn(t.size)
+            xc = x*np.exp(1j*np.pi/4)
+
+            # real signal
+            _, _, z = stft(x, nperseg=nperseg, noverlap=noverlap, nfft=nfft,
+                            window=window, detrend=None, padded=True)
+
+            # complex signal
+            _, _, zc = stft(xc, nperseg=nperseg, noverlap=noverlap, nfft=nfft,
+                            window=window, detrend=None, padded=True,
+                            return_onesided=False)
+
+            tr, xr = istft(z, nperseg=nperseg, noverlap=noverlap, nfft=nfft,
+                           window=window)
+
+            tr, xcr = istft(zc, nperseg=nperseg, noverlap=noverlap, nfft=nfft,
+                            window=window, input_onesided=False)
+
+            msg = f'{window}, {noverlap}'
+            assert_allclose(t, tr, err_msg=msg)
+            assert_allclose(x, xr, err_msg=msg)
+            assert_allclose(xc, xcr, err_msg=msg)
+
+    def test_axis_rolling(self):
+        np.random.seed(1234)
+
+        x_flat = np.random.randn(1024)
+        _, _, z_flat = stft(x_flat)
+
+        for a in range(3):
+            newshape = [1,]*3
+            newshape[a] = -1
+            x = x_flat.reshape(newshape)
+
+            _, _, z_plus = stft(x, axis=a)  # Positive axis index
+            _, _, z_minus = stft(x, axis=a-x.ndim)  # Negative axis index
+
+            assert_equal(z_flat, z_plus.squeeze(), err_msg=a)
+            assert_equal(z_flat, z_minus.squeeze(), err_msg=a-x.ndim)
+
+        # z_flat has shape [n_freq, n_time]
+
+        # Test vs. transpose
+        _, x_transpose_m = istft(z_flat.T, time_axis=-2, freq_axis=-1)
+        _, x_transpose_p = istft(z_flat.T, time_axis=0, freq_axis=1)
+
+        assert_allclose(x_flat, x_transpose_m, err_msg='istft transpose minus')
+        assert_allclose(x_flat, x_transpose_p, err_msg='istft transpose plus')
+
+    def test_roundtrip_scaling(self):
+        """Verify behavior of scaling parameter. """
+        # Create 1024 sample cosine signal with amplitude 2:
+        X = np.zeros(513, dtype=complex)
+        X[256] = 1024
+        x = np.fft.irfft(X)
+        power_x = sum(x**2) / len(x)  # power of signal x is 2
+
+        # Calculate magnitude-scaled STFT:
+        Zs = stft(x, boundary='even', scaling='spectrum')[2]
+
+        # Test round trip:
+        x1 = istft(Zs, boundary=True, scaling='spectrum')[1]
+        assert_allclose(x1, x)
+
+        # For a Hann-windowed 256 sample length FFT, we expect a peak at
+        # frequency 64 (since it is 1/4 the length of X) with a height of 1
+        # (half the amplitude). A Hann window of a perfectly centered sine has
+        # the magnitude [..., 0, 0, 0.5, 1, 0.5, 0, 0, ...].
+        # Note that in this case the 'even' padding works for the beginning
+        # but not for the end of the STFT.
+        assert_allclose(abs(Zs[63, :-1]), 0.5)
+        assert_allclose(abs(Zs[64, :-1]), 1)
+        assert_allclose(abs(Zs[65, :-1]), 0.5)
+        # All other values should be zero:
+        Zs[63:66, :-1] = 0
+        # Note since 'rtol' does not have influence here, atol needs to be set:
+        assert_allclose(Zs[:, :-1], 0, atol=np.finfo(Zs.dtype).resolution)
+
+        # Calculate two-sided psd-scaled STFT:
+        #  - using 'even' padding since signal is axis symmetric - this ensures
+        #    stationary behavior on the boundaries
+        #  - using the two-sided transform allows determining the spectral
+        #    power by `sum(abs(Zp[:, k])**2) / len(f)` for the k-th time slot.
+        Zp = stft(x, return_onesided=False, boundary='even', scaling='psd')[2]
+
+        # Calculate spectral power of Zd by summing over the frequency axis:
+        psd_Zp = np.sum(Zp.real**2 + Zp.imag**2, axis=0) / Zp.shape[0]
+        # Spectral power of Zp should be equal to the signal's power:
+        assert_allclose(psd_Zp, power_x)
+
+        # Test round trip:
+        x1 = istft(Zp, input_onesided=False, boundary=True, scaling='psd')[1]
+        assert_allclose(x1, x)
+
+        # The power of the one-sided psd-scaled STFT can be determined
+        # analogously (note that the two sides are not of equal shape):
+        Zp0 = stft(x, return_onesided=True, boundary='even', scaling='psd')[2]
+
+        # Since x is real, its Fourier transform is conjugate symmetric, i.e.,
+        # the missing 'second side' can be expressed through the 'first side':
+        Zp1 = np.conj(Zp0[-2:0:-1, :])  # 'second side' is conjugate reversed
+        assert_allclose(Zp[:129, :], Zp0)
+        assert_allclose(Zp[129:, :], Zp1)
+
+        # Calculate the spectral power:
+        s2 = (np.sum(Zp0.real ** 2 + Zp0.imag ** 2, axis=0) +
+              np.sum(Zp1.real ** 2 + Zp1.imag ** 2, axis=0))
+        psd_Zp01 = s2 / (Zp0.shape[0] + Zp1.shape[0])
+        assert_allclose(psd_Zp01, power_x)
+
+        # Test round trip:
+        x1 = istft(Zp0, input_onesided=True, boundary=True, scaling='psd')[1]
+        assert_allclose(x1, x)
+
+
+class TestSampledSpectralRepresentations:
+    """Check energy/power relations from `Spectral Analysis` section in the user guide.
+
+    A 32 sample cosine signal is used to compare the numerical to the expected results
+    stated in :ref:`tutorial_SpectralAnalysis` in
+    file ``doc/source/tutorial/signal.rst``
+    """
+    n: int = 32  #: number of samples
+    T: float = 1/16  #: sampling interval
+    a_ref: float = 3  #: amplitude of reference
+    l_a: int = 3  #: index in fft for defining frequency of test signal
+
+    x_ref: np.ndarray  #: reference signal
+    X_ref: np.ndarray  #: two-sided FFT of x_ref
+    E_ref: float  #: energy of signal
+    P_ref: float  #: power of signal
+
+    def setup_method(self):
+        """Create Cosine signal with amplitude a from spectrum. """
+        f = rfftfreq(self.n, self.T)
+        X_ref = np.zeros_like(f)
+        self.l_a = 3
+        X_ref[self.l_a] = self.a_ref/2 * self.n  # set amplitude
+        self.x_ref = irfft(X_ref)
+        self.X_ref = fft(self.x_ref)
+
+        # Closed form expression for continuous-time signal:
+        self.E_ref = self.tau * self.a_ref**2 / 2  # energy of signal
+        self.P_ref = self.a_ref**2 / 2  # power of signal
+
+    @property
+    def tau(self) -> float:
+        """Duration of signal. """
+        return self.n * self.T
+
+    @property
+    def delta_f(self) -> float:
+        """Bin width """
+        return 1 / (self.n * self.T)
+
+    def test_reference_signal(self):
+        """Test energy and power formulas. """
+        # Verify that amplitude is a:
+        assert_allclose(2*self.a_ref, np.ptp(self.x_ref), rtol=0.1)
+        # Verify that energy expression for sampled signal:
+        assert_allclose(self.T * sum(self.x_ref ** 2), self.E_ref)
+
+        # Verify that spectral energy and power formulas are correct:
+        sum_X_ref_squared = sum(self.X_ref.real**2 + self.X_ref.imag**2)
+        assert_allclose(self.T/self.n * sum_X_ref_squared, self.E_ref)
+        assert_allclose(1/self.n**2 * sum_X_ref_squared, self.P_ref)
+
+    def test_windowed_DFT(self):
+        """Verify spectral representations of windowed DFT.
+
+        Furthermore, the scalings of `periodogram` and `welch` are verified.
+        """
+        w = hann(self.n, sym=False)
+        c_amp, c_rms = abs(sum(w)), np.sqrt(sum(w.real**2 + w.imag**2))
+        Xw = fft(self.x_ref*w)  # unnormalized windowed DFT
+
+        # Verify that the *spectrum* peak is consistent:
+        assert_allclose(self.tau * Xw[self.l_a] / c_amp, self.a_ref * self.tau / 2)
+        # Verify that the *amplitude spectrum* peak is consistent:
+        assert_allclose(Xw[self.l_a] / c_amp, self.a_ref/2)
+
+        # Verify spectral power/energy equals signal's power/energy:
+        X_ESD = self.tau * self.T * abs(Xw / c_rms)**2  # Energy Spectral Density
+        X_PSD = self.T * abs(Xw / c_rms)**2  # Power Spectral Density
+        assert_allclose(self.delta_f * sum(X_ESD), self.E_ref)
+        assert_allclose(self.delta_f * sum(X_PSD), self.P_ref)
+
+        # Verify scalings of periodogram:
+        kw = dict(fs=1/self.T, window=w, detrend=False, return_onesided=False)
+        _, P_mag = periodogram(self.x_ref, scaling='spectrum', **kw)
+        _, P_psd = periodogram(self.x_ref, scaling='density', **kw)
+
+        # Verify that periodogram calculates a squared magnitude spectrum:
+        float_res = np.finfo(P_mag.dtype).resolution
+        assert_allclose(P_mag, abs(Xw/c_amp)**2, atol=float_res*max(P_mag))
+        # Verify that periodogram calculates a PSD:
+        assert_allclose(P_psd, X_PSD, atol=float_res*max(P_psd))
+
+        # Ensure that scaling of welch is the same as of periodogram:
+        kw = dict(nperseg=len(self.x_ref), noverlap=0, **kw)
+        assert_allclose(welch(self.x_ref, scaling='spectrum', **kw)[1], P_mag,
+                        atol=float_res*max(P_mag))
+        assert_allclose(welch(self.x_ref, scaling='density', **kw)[1], P_psd,
+                        atol=float_res*max(P_psd))
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_wavelets.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_wavelets.py
new file mode 100644
index 0000000000000000000000000000000000000000..e83e6918429bfc539a44fc9a627deabafe2852a6
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_wavelets.py
@@ -0,0 +1,161 @@
+import numpy as np
+from numpy.testing import (assert_equal,
+    assert_array_equal, assert_array_almost_equal, assert_array_less, assert_,)
+import pytest
+
+import scipy.signal._wavelets as wavelets
+
+
+class TestWavelets:
+    def test_qmf(self):
+        with pytest.deprecated_call():
+            assert_array_equal(wavelets.qmf([1, 1]), [1, -1])
+
+    def test_daub(self):
+        with pytest.deprecated_call():
+            for i in range(1, 15):
+                assert_equal(len(wavelets.daub(i)), i * 2)
+
+    def test_cascade(self):
+        with pytest.deprecated_call():
+            for J in range(1, 7):
+                for i in range(1, 5):
+                    lpcoef = wavelets.daub(i)
+                    k = len(lpcoef)
+                    x, phi, psi = wavelets.cascade(lpcoef, J)
+                    assert_(len(x) == len(phi) == len(psi))
+                    assert_equal(len(x), (k - 1) * 2 ** J)
+
+    def test_morlet(self):
+        with pytest.deprecated_call():
+            x = wavelets.morlet(50, 4.1, complete=True)
+            y = wavelets.morlet(50, 4.1, complete=False)
+            # Test if complete and incomplete wavelet have same lengths:
+            assert_equal(len(x), len(y))
+            # Test if complete wavelet is less than incomplete wavelet:
+            assert_array_less(x, y)
+
+            x = wavelets.morlet(10, 50, complete=False)
+            y = wavelets.morlet(10, 50, complete=True)
+            # For large widths complete and incomplete wavelets should be
+            # identical within numerical precision:
+            assert_equal(x, y)
+
+            # miscellaneous tests:
+            x = np.array([1.73752399e-09 + 9.84327394e-25j,
+                          6.49471756e-01 + 0.00000000e+00j,
+                          1.73752399e-09 - 9.84327394e-25j])
+            y = wavelets.morlet(3, w=2, complete=True)
+            assert_array_almost_equal(x, y)
+
+            x = np.array([2.00947715e-09 + 9.84327394e-25j,
+                          7.51125544e-01 + 0.00000000e+00j,
+                          2.00947715e-09 - 9.84327394e-25j])
+            y = wavelets.morlet(3, w=2, complete=False)
+            assert_array_almost_equal(x, y, decimal=2)
+
+            x = wavelets.morlet(10000, s=4, complete=True)
+            y = wavelets.morlet(20000, s=8, complete=True)[5000:15000]
+            assert_array_almost_equal(x, y, decimal=2)
+
+            x = wavelets.morlet(10000, s=4, complete=False)
+            assert_array_almost_equal(y, x, decimal=2)
+            y = wavelets.morlet(20000, s=8, complete=False)[5000:15000]
+            assert_array_almost_equal(x, y, decimal=2)
+
+            x = wavelets.morlet(10000, w=3, s=5, complete=True)
+            y = wavelets.morlet(20000, w=3, s=10, complete=True)[5000:15000]
+            assert_array_almost_equal(x, y, decimal=2)
+
+            x = wavelets.morlet(10000, w=3, s=5, complete=False)
+            assert_array_almost_equal(y, x, decimal=2)
+            y = wavelets.morlet(20000, w=3, s=10, complete=False)[5000:15000]
+            assert_array_almost_equal(x, y, decimal=2)
+
+            x = wavelets.morlet(10000, w=7, s=10, complete=True)
+            y = wavelets.morlet(20000, w=7, s=20, complete=True)[5000:15000]
+            assert_array_almost_equal(x, y, decimal=2)
+
+            x = wavelets.morlet(10000, w=7, s=10, complete=False)
+            assert_array_almost_equal(x, y, decimal=2)
+            y = wavelets.morlet(20000, w=7, s=20, complete=False)[5000:15000]
+            assert_array_almost_equal(x, y, decimal=2)
+
+    def test_morlet2(self):
+        with pytest.deprecated_call():
+            w = wavelets.morlet2(1.0, 0.5)
+            expected = (np.pi**(-0.25) * np.sqrt(1/0.5)).astype(complex)
+            assert_array_equal(w, expected)
+
+            lengths = [5, 11, 15, 51, 101]
+            for length in lengths:
+                w = wavelets.morlet2(length, 1.0)
+                assert_(len(w) == length)
+                max_loc = np.argmax(w)
+                assert_(max_loc == (length // 2))
+
+            points = 100
+            w = abs(wavelets.morlet2(points, 2.0))
+            half_vec = np.arange(0, points // 2)
+            assert_array_almost_equal(w[half_vec], w[-(half_vec + 1)])
+
+            x = np.array([5.03701224e-09 + 2.46742437e-24j,
+                          1.88279253e+00 + 0.00000000e+00j,
+                          5.03701224e-09 - 2.46742437e-24j])
+            y = wavelets.morlet2(3, s=1/(2*np.pi), w=2)
+            assert_array_almost_equal(x, y)
+
+    def test_ricker(self):
+        with pytest.deprecated_call():
+            w = wavelets.ricker(1.0, 1)
+            expected = 2 / (np.sqrt(3 * 1.0) * (np.pi ** 0.25))
+            assert_array_equal(w, expected)
+
+            lengths = [5, 11, 15, 51, 101]
+            for length in lengths:
+                w = wavelets.ricker(length, 1.0)
+                assert_(len(w) == length)
+                max_loc = np.argmax(w)
+                assert_(max_loc == (length // 2))
+
+            points = 100
+            w = wavelets.ricker(points, 2.0)
+            half_vec = np.arange(0, points // 2)
+            #Wavelet should be symmetric
+            assert_array_almost_equal(w[half_vec], w[-(half_vec + 1)])
+
+            #Check zeros
+            aas = [5, 10, 15, 20, 30]
+            points = 99
+            for a in aas:
+                w = wavelets.ricker(points, a)
+                vec = np.arange(0, points) - (points - 1.0) / 2
+                exp_zero1 = np.argmin(np.abs(vec - a))
+                exp_zero2 = np.argmin(np.abs(vec + a))
+                assert_array_almost_equal(w[exp_zero1], 0)
+                assert_array_almost_equal(w[exp_zero2], 0)
+
+    def test_cwt(self):
+        with pytest.deprecated_call():
+            widths = [1.0]
+            def delta_wavelet(s, t):
+                return np.array([1])
+            len_data = 100
+            test_data = np.sin(np.pi * np.arange(0, len_data) / 10.0)
+
+            #Test delta function input gives same data as output
+            cwt_dat = wavelets.cwt(test_data, delta_wavelet, widths)
+            assert_(cwt_dat.shape == (len(widths), len_data))
+            assert_array_almost_equal(test_data, cwt_dat.flatten())
+
+            #Check proper shape on output
+            widths = [1, 3, 4, 5, 10]
+            cwt_dat = wavelets.cwt(test_data, wavelets.ricker, widths)
+            assert_(cwt_dat.shape == (len(widths), len_data))
+
+            widths = [len_data * 10]
+            #Note: this wavelet isn't defined quite right, but is fine for this test
+            def flat_wavelet(l, w):
+                return np.full(w, 1 / w)
+            cwt_dat = wavelets.cwt(test_data, flat_wavelet, widths)
+            assert_array_almost_equal(cwt_dat, np.mean(test_data))
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_windows.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_windows.py
new file mode 100644
index 0000000000000000000000000000000000000000..cdf1a46c924f9f2594d944e885b23c37fe9bf4f2
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/tests/test_windows.py
@@ -0,0 +1,846 @@
+import numpy as np
+from numpy import array
+from numpy.testing import (assert_array_almost_equal, assert_array_equal,
+                           assert_allclose,
+                           assert_equal, assert_, assert_array_less,
+                           suppress_warnings)
+from pytest import raises as assert_raises
+
+from scipy.fft import fft
+from scipy.signal import windows, get_window, resample
+
+
+window_funcs = [
+    ('boxcar', ()),
+    ('triang', ()),
+    ('parzen', ()),
+    ('bohman', ()),
+    ('blackman', ()),
+    ('nuttall', ()),
+    ('blackmanharris', ()),
+    ('flattop', ()),
+    ('bartlett', ()),
+    ('barthann', ()),
+    ('hamming', ()),
+    ('kaiser', (1,)),
+    ('dpss', (2,)),
+    ('gaussian', (0.5,)),
+    ('general_gaussian', (1.5, 2)),
+    ('chebwin', (1,)),
+    ('cosine', ()),
+    ('hann', ()),
+    ('exponential', ()),
+    ('taylor', ()),
+    ('tukey', (0.5,)),
+    ('lanczos', ()),
+    ]
+
+
+class TestBartHann:
+
+    def test_basic(self):
+        assert_allclose(windows.barthann(6, sym=True),
+                        [0, 0.35857354213752, 0.8794264578624801,
+                         0.8794264578624801, 0.3585735421375199, 0],
+                        rtol=1e-15, atol=1e-15)
+        assert_allclose(windows.barthann(7),
+                        [0, 0.27, 0.73, 1.0, 0.73, 0.27, 0],
+                        rtol=1e-15, atol=1e-15)
+        assert_allclose(windows.barthann(6, False),
+                        [0, 0.27, 0.73, 1.0, 0.73, 0.27],
+                        rtol=1e-15, atol=1e-15)
+
+
+class TestBartlett:
+
+    def test_basic(self):
+        assert_allclose(windows.bartlett(6), [0, 0.4, 0.8, 0.8, 0.4, 0])
+        assert_allclose(windows.bartlett(7), [0, 1/3, 2/3, 1.0, 2/3, 1/3, 0])
+        assert_allclose(windows.bartlett(6, False),
+                        [0, 1/3, 2/3, 1.0, 2/3, 1/3])
+
+
+class TestBlackman:
+
+    def test_basic(self):
+        assert_allclose(windows.blackman(6, sym=False),
+                        [0, 0.13, 0.63, 1.0, 0.63, 0.13], atol=1e-14)
+        assert_allclose(windows.blackman(7, sym=False),
+                        [0, 0.09045342435412804, 0.4591829575459636,
+                         0.9203636180999081, 0.9203636180999081,
+                         0.4591829575459636, 0.09045342435412804], atol=1e-8)
+        assert_allclose(windows.blackman(6),
+                        [0, 0.2007701432625305, 0.8492298567374694,
+                         0.8492298567374694, 0.2007701432625305, 0],
+                        atol=1e-14)
+        assert_allclose(windows.blackman(7, True),
+                        [0, 0.13, 0.63, 1.0, 0.63, 0.13, 0], atol=1e-14)
+
+
+class TestBlackmanHarris:
+
+    def test_basic(self):
+        assert_allclose(windows.blackmanharris(6, False),
+                        [6.0e-05, 0.055645, 0.520575, 1.0, 0.520575, 0.055645])
+        assert_allclose(windows.blackmanharris(7, sym=False),
+                        [6.0e-05, 0.03339172347815117, 0.332833504298565,
+                         0.8893697722232837, 0.8893697722232838,
+                         0.3328335042985652, 0.03339172347815122])
+        assert_allclose(windows.blackmanharris(6),
+                        [6.0e-05, 0.1030114893456638, 0.7938335106543362,
+                         0.7938335106543364, 0.1030114893456638, 6.0e-05])
+        assert_allclose(windows.blackmanharris(7, sym=True),
+                        [6.0e-05, 0.055645, 0.520575, 1.0, 0.520575, 0.055645,
+                         6.0e-05])
+
+
+class TestTaylor:
+
+    def test_normalized(self):
+        """Tests windows of small length that are normalized to 1. See the
+        documentation for the Taylor window for more information on
+        normalization.
+        """
+        assert_allclose(windows.taylor(1, 2, 15), 1.0)
+        assert_allclose(
+            windows.taylor(5, 2, 15),
+            np.array([0.75803341, 0.90757699, 1.0, 0.90757699, 0.75803341])
+        )
+        assert_allclose(
+            windows.taylor(6, 2, 15),
+            np.array([
+                0.7504082, 0.86624416, 0.98208011, 0.98208011, 0.86624416,
+                0.7504082
+            ])
+        )
+
+    def test_non_normalized(self):
+        """Test windows of small length that are not normalized to 1. See
+        the documentation for the Taylor window for more information on
+        normalization.
+        """
+        assert_allclose(
+            windows.taylor(5, 2, 15, norm=False),
+            np.array([
+                0.87508054, 1.04771499, 1.15440894, 1.04771499, 0.87508054
+            ])
+        )
+        assert_allclose(
+            windows.taylor(6, 2, 15, norm=False),
+            np.array([
+                0.86627793, 1.0, 1.13372207, 1.13372207, 1.0, 0.86627793
+            ])
+        )
+
+    def test_correctness(self):
+        """This test ensures the correctness of the implemented Taylor
+        Windowing function. A Taylor Window of 1024 points is created, its FFT
+        is taken, and the Peak Sidelobe Level (PSLL) and 3dB and 18dB bandwidth
+        are found and checked.
+
+        A publication from Sandia National Laboratories was used as reference
+        for the correctness values [1]_.
+
+        References
+        -----
+        .. [1] Armin Doerry, "Catalog of Window Taper Functions for
+               Sidelobe Control", 2017.
+               https://www.researchgate.net/profile/Armin_Doerry/publication/316281181_Catalog_of_Window_Taper_Functions_for_Sidelobe_Control/links/58f92cb2a6fdccb121c9d54d/Catalog-of-Window-Taper-Functions-for-Sidelobe-Control.pdf
+        """
+        M_win = 1024
+        N_fft = 131072
+        # Set norm=False for correctness as the values obtained from the
+        # scientific publication do not normalize the values. Normalizing
+        # changes the sidelobe level from the desired value.
+        w = windows.taylor(M_win, nbar=4, sll=35, norm=False, sym=False)
+        f = fft(w, N_fft)
+        spec = 20 * np.log10(np.abs(f / np.amax(f)))
+
+        first_zero = np.argmax(np.diff(spec) > 0)
+
+        PSLL = np.amax(spec[first_zero:-first_zero])
+
+        BW_3dB = 2*np.argmax(spec <= -3.0102999566398121) / N_fft * M_win
+        BW_18dB = 2*np.argmax(spec <= -18.061799739838872) / N_fft * M_win
+
+        assert_allclose(PSLL, -35.1672, atol=1)
+        assert_allclose(BW_3dB, 1.1822, atol=0.1)
+        assert_allclose(BW_18dB, 2.6112, atol=0.1)
+
+
+class TestBohman:
+
+    def test_basic(self):
+        assert_allclose(windows.bohman(6),
+                        [0, 0.1791238937062839, 0.8343114522576858,
+                         0.8343114522576858, 0.1791238937062838, 0])
+        assert_allclose(windows.bohman(7, sym=True),
+                        [0, 0.1089977810442293, 0.6089977810442293, 1.0,
+                         0.6089977810442295, 0.1089977810442293, 0])
+        assert_allclose(windows.bohman(6, False),
+                        [0, 0.1089977810442293, 0.6089977810442293, 1.0,
+                         0.6089977810442295, 0.1089977810442293])
+
+
+class TestBoxcar:
+
+    def test_basic(self):
+        assert_allclose(windows.boxcar(6), [1, 1, 1, 1, 1, 1])
+        assert_allclose(windows.boxcar(7), [1, 1, 1, 1, 1, 1, 1])
+        assert_allclose(windows.boxcar(6, False), [1, 1, 1, 1, 1, 1])
+
+
+cheb_odd_true = array([0.200938, 0.107729, 0.134941, 0.165348,
+                       0.198891, 0.235450, 0.274846, 0.316836,
+                       0.361119, 0.407338, 0.455079, 0.503883,
+                       0.553248, 0.602637, 0.651489, 0.699227,
+                       0.745266, 0.789028, 0.829947, 0.867485,
+                       0.901138, 0.930448, 0.955010, 0.974482,
+                       0.988591, 0.997138, 1.000000, 0.997138,
+                       0.988591, 0.974482, 0.955010, 0.930448,
+                       0.901138, 0.867485, 0.829947, 0.789028,
+                       0.745266, 0.699227, 0.651489, 0.602637,
+                       0.553248, 0.503883, 0.455079, 0.407338,
+                       0.361119, 0.316836, 0.274846, 0.235450,
+                       0.198891, 0.165348, 0.134941, 0.107729,
+                       0.200938])
+
+cheb_even_true = array([0.203894, 0.107279, 0.133904,
+                        0.163608, 0.196338, 0.231986,
+                        0.270385, 0.311313, 0.354493,
+                        0.399594, 0.446233, 0.493983,
+                        0.542378, 0.590916, 0.639071,
+                        0.686302, 0.732055, 0.775783,
+                        0.816944, 0.855021, 0.889525,
+                        0.920006, 0.946060, 0.967339,
+                        0.983557, 0.994494, 1.000000,
+                        1.000000, 0.994494, 0.983557,
+                        0.967339, 0.946060, 0.920006,
+                        0.889525, 0.855021, 0.816944,
+                        0.775783, 0.732055, 0.686302,
+                        0.639071, 0.590916, 0.542378,
+                        0.493983, 0.446233, 0.399594,
+                        0.354493, 0.311313, 0.270385,
+                        0.231986, 0.196338, 0.163608,
+                        0.133904, 0.107279, 0.203894])
+
+
+class TestChebWin:
+
+    def test_basic(self):
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning, "This window is not suitable")
+            assert_allclose(windows.chebwin(6, 100),
+                            [0.1046401879356917, 0.5075781475823447, 1.0, 1.0,
+                             0.5075781475823447, 0.1046401879356917])
+            assert_allclose(windows.chebwin(7, 100),
+                            [0.05650405062850233, 0.316608530648474,
+                             0.7601208123539079, 1.0, 0.7601208123539079,
+                             0.316608530648474, 0.05650405062850233])
+            assert_allclose(windows.chebwin(6, 10),
+                            [1.0, 0.6071201674458373, 0.6808391469897297,
+                             0.6808391469897297, 0.6071201674458373, 1.0])
+            assert_allclose(windows.chebwin(7, 10),
+                            [1.0, 0.5190521247588651, 0.5864059018130382,
+                             0.6101519801307441, 0.5864059018130382,
+                             0.5190521247588651, 1.0])
+            assert_allclose(windows.chebwin(6, 10, False),
+                            [1.0, 0.5190521247588651, 0.5864059018130382,
+                             0.6101519801307441, 0.5864059018130382,
+                             0.5190521247588651])
+
+    def test_cheb_odd_high_attenuation(self):
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning, "This window is not suitable")
+            cheb_odd = windows.chebwin(53, at=-40)
+        assert_array_almost_equal(cheb_odd, cheb_odd_true, decimal=4)
+
+    def test_cheb_even_high_attenuation(self):
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning, "This window is not suitable")
+            cheb_even = windows.chebwin(54, at=40)
+        assert_array_almost_equal(cheb_even, cheb_even_true, decimal=4)
+
+    def test_cheb_odd_low_attenuation(self):
+        cheb_odd_low_at_true = array([1.000000, 0.519052, 0.586405,
+                                      0.610151, 0.586405, 0.519052,
+                                      1.000000])
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning, "This window is not suitable")
+            cheb_odd = windows.chebwin(7, at=10)
+        assert_array_almost_equal(cheb_odd, cheb_odd_low_at_true, decimal=4)
+
+    def test_cheb_even_low_attenuation(self):
+        cheb_even_low_at_true = array([1.000000, 0.451924, 0.51027,
+                                       0.541338, 0.541338, 0.51027,
+                                       0.451924, 1.000000])
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning, "This window is not suitable")
+            cheb_even = windows.chebwin(8, at=-10)
+        assert_array_almost_equal(cheb_even, cheb_even_low_at_true, decimal=4)
+
+
+exponential_data = {
+    (4, None, 0.2, False):
+        array([4.53999297624848542e-05,
+               6.73794699908546700e-03, 1.00000000000000000e+00,
+               6.73794699908546700e-03]),
+    (4, None, 0.2, True): array([0.00055308437014783, 0.0820849986238988,
+                                 0.0820849986238988, 0.00055308437014783]),
+    (4, None, 1.0, False): array([0.1353352832366127, 0.36787944117144233, 1.,
+                                  0.36787944117144233]),
+    (4, None, 1.0, True): array([0.22313016014842982, 0.60653065971263342,
+                                 0.60653065971263342, 0.22313016014842982]),
+    (4, 2, 0.2, False):
+        array([4.53999297624848542e-05, 6.73794699908546700e-03,
+               1.00000000000000000e+00, 6.73794699908546700e-03]),
+    (4, 2, 0.2, True): None,
+    (4, 2, 1.0, False): array([0.1353352832366127, 0.36787944117144233, 1.,
+                               0.36787944117144233]),
+    (4, 2, 1.0, True): None,
+    (5, None, 0.2, True):
+        array([4.53999297624848542e-05,
+               6.73794699908546700e-03, 1.00000000000000000e+00,
+               6.73794699908546700e-03, 4.53999297624848542e-05]),
+    (5, None, 1.0, True): array([0.1353352832366127, 0.36787944117144233, 1.,
+                                 0.36787944117144233, 0.1353352832366127]),
+    (5, 2, 0.2, True): None,
+    (5, 2, 1.0, True): None
+}
+
+
+def test_exponential():
+    for k, v in exponential_data.items():
+        if v is None:
+            assert_raises(ValueError, windows.exponential, *k)
+        else:
+            win = windows.exponential(*k)
+            assert_allclose(win, v, rtol=1e-14)
+
+
+class TestFlatTop:
+
+    def test_basic(self):
+        assert_allclose(windows.flattop(6, sym=False),
+                        [-0.000421051, -0.051263156, 0.19821053, 1.0,
+                         0.19821053, -0.051263156])
+        assert_allclose(windows.flattop(7, sym=False),
+                        [-0.000421051, -0.03684078115492348,
+                         0.01070371671615342, 0.7808739149387698,
+                         0.7808739149387698, 0.01070371671615342,
+                         -0.03684078115492348])
+        assert_allclose(windows.flattop(6),
+                        [-0.000421051, -0.0677142520762119, 0.6068721525762117,
+                         0.6068721525762117, -0.0677142520762119,
+                         -0.000421051])
+        assert_allclose(windows.flattop(7, True),
+                        [-0.000421051, -0.051263156, 0.19821053, 1.0,
+                         0.19821053, -0.051263156, -0.000421051])
+
+
+class TestGaussian:
+
+    def test_basic(self):
+        assert_allclose(windows.gaussian(6, 1.0),
+                        [0.04393693362340742, 0.3246524673583497,
+                         0.8824969025845955, 0.8824969025845955,
+                         0.3246524673583497, 0.04393693362340742])
+        assert_allclose(windows.gaussian(7, 1.2),
+                        [0.04393693362340742, 0.2493522087772962,
+                         0.7066482778577162, 1.0, 0.7066482778577162,
+                         0.2493522087772962, 0.04393693362340742])
+        assert_allclose(windows.gaussian(7, 3),
+                        [0.6065306597126334, 0.8007374029168081,
+                         0.9459594689067654, 1.0, 0.9459594689067654,
+                         0.8007374029168081, 0.6065306597126334])
+        assert_allclose(windows.gaussian(6, 3, False),
+                        [0.6065306597126334, 0.8007374029168081,
+                         0.9459594689067654, 1.0, 0.9459594689067654,
+                         0.8007374029168081])
+
+
+class TestGeneralCosine:
+
+    def test_basic(self):
+        assert_allclose(windows.general_cosine(5, [0.5, 0.3, 0.2]),
+                        [0.4, 0.3, 1, 0.3, 0.4])
+        assert_allclose(windows.general_cosine(4, [0.5, 0.3, 0.2], sym=False),
+                        [0.4, 0.3, 1, 0.3])
+
+
+class TestGeneralHamming:
+
+    def test_basic(self):
+        assert_allclose(windows.general_hamming(5, 0.7),
+                        [0.4, 0.7, 1.0, 0.7, 0.4])
+        assert_allclose(windows.general_hamming(5, 0.75, sym=False),
+                        [0.5, 0.6727457514, 0.9522542486,
+                         0.9522542486, 0.6727457514])
+        assert_allclose(windows.general_hamming(6, 0.75, sym=True),
+                        [0.5, 0.6727457514, 0.9522542486,
+                        0.9522542486, 0.6727457514, 0.5])
+
+
+class TestHamming:
+
+    def test_basic(self):
+        assert_allclose(windows.hamming(6, False),
+                        [0.08, 0.31, 0.77, 1.0, 0.77, 0.31])
+        assert_allclose(windows.hamming(7, sym=False),
+                        [0.08, 0.2531946911449826, 0.6423596296199047,
+                         0.9544456792351128, 0.9544456792351128,
+                         0.6423596296199047, 0.2531946911449826])
+        assert_allclose(windows.hamming(6),
+                        [0.08, 0.3978521825875242, 0.9121478174124757,
+                         0.9121478174124757, 0.3978521825875242, 0.08])
+        assert_allclose(windows.hamming(7, sym=True),
+                        [0.08, 0.31, 0.77, 1.0, 0.77, 0.31, 0.08])
+
+
+class TestHann:
+
+    def test_basic(self):
+        assert_allclose(windows.hann(6, sym=False),
+                        [0, 0.25, 0.75, 1.0, 0.75, 0.25],
+                        rtol=1e-15, atol=1e-15)
+        assert_allclose(windows.hann(7, sym=False),
+                        [0, 0.1882550990706332, 0.6112604669781572,
+                         0.9504844339512095, 0.9504844339512095,
+                         0.6112604669781572, 0.1882550990706332],
+                        rtol=1e-15, atol=1e-15)
+        assert_allclose(windows.hann(6, True),
+                        [0, 0.3454915028125263, 0.9045084971874737,
+                         0.9045084971874737, 0.3454915028125263, 0],
+                        rtol=1e-15, atol=1e-15)
+        assert_allclose(windows.hann(7),
+                        [0, 0.25, 0.75, 1.0, 0.75, 0.25, 0],
+                        rtol=1e-15, atol=1e-15)
+
+
+class TestKaiser:
+
+    def test_basic(self):
+        assert_allclose(windows.kaiser(6, 0.5),
+                        [0.9403061933191572, 0.9782962393705389,
+                         0.9975765035372042, 0.9975765035372042,
+                         0.9782962393705389, 0.9403061933191572])
+        assert_allclose(windows.kaiser(7, 0.5),
+                        [0.9403061933191572, 0.9732402256999829,
+                         0.9932754654413773, 1.0, 0.9932754654413773,
+                         0.9732402256999829, 0.9403061933191572])
+        assert_allclose(windows.kaiser(6, 2.7),
+                        [0.2603047507678832, 0.6648106293528054,
+                         0.9582099802511439, 0.9582099802511439,
+                         0.6648106293528054, 0.2603047507678832])
+        assert_allclose(windows.kaiser(7, 2.7),
+                        [0.2603047507678832, 0.5985765418119844,
+                         0.8868495172060835, 1.0, 0.8868495172060835,
+                         0.5985765418119844, 0.2603047507678832])
+        assert_allclose(windows.kaiser(6, 2.7, False),
+                        [0.2603047507678832, 0.5985765418119844,
+                         0.8868495172060835, 1.0, 0.8868495172060835,
+                         0.5985765418119844])
+
+
+class TestKaiserBesselDerived:
+
+    def test_basic(self):
+        M = 100
+        w = windows.kaiser_bessel_derived(M, beta=4.0)
+        w2 = windows.get_window(('kaiser bessel derived', 4.0),
+                                M, fftbins=False)
+        assert_allclose(w, w2)
+
+        # Test for Princen-Bradley condition
+        assert_allclose(w[:M // 2] ** 2 + w[-M // 2:] ** 2, 1.)
+
+        # Test actual values from other implementations
+        # M = 2:  sqrt(2) / 2
+        # M = 4:  0.518562710536, 0.855039598640
+        # M = 6:  0.436168993154, 0.707106781187, 0.899864772847
+        # Ref:https://github.com/scipy/scipy/pull/4747#issuecomment-172849418
+        assert_allclose(windows.kaiser_bessel_derived(2, beta=np.pi / 2)[:1],
+                        np.sqrt(2) / 2)
+
+        assert_allclose(windows.kaiser_bessel_derived(4, beta=np.pi / 2)[:2],
+                        [0.518562710536, 0.855039598640])
+
+        assert_allclose(windows.kaiser_bessel_derived(6, beta=np.pi / 2)[:3],
+                        [0.436168993154, 0.707106781187, 0.899864772847])
+
+    def test_exceptions(self):
+        M = 100
+        # Assert ValueError for odd window length
+        msg = ("Kaiser-Bessel Derived windows are only defined for even "
+               "number of points")
+        with assert_raises(ValueError, match=msg):
+            windows.kaiser_bessel_derived(M + 1, beta=4.)
+
+        # Assert ValueError for non-symmetric setting
+        msg = ("Kaiser-Bessel Derived windows are only defined for "
+               "symmetric shapes")
+        with assert_raises(ValueError, match=msg):
+            windows.kaiser_bessel_derived(M + 1, beta=4., sym=False)
+
+
+class TestNuttall:
+
+    def test_basic(self):
+        assert_allclose(windows.nuttall(6, sym=False),
+                        [0.0003628, 0.0613345, 0.5292298, 1.0, 0.5292298,
+                         0.0613345])
+        assert_allclose(windows.nuttall(7, sym=False),
+                        [0.0003628, 0.03777576895352025, 0.3427276199688195,
+                         0.8918518610776603, 0.8918518610776603,
+                         0.3427276199688196, 0.0377757689535203])
+        assert_allclose(windows.nuttall(6),
+                        [0.0003628, 0.1105152530498718, 0.7982580969501282,
+                         0.7982580969501283, 0.1105152530498719, 0.0003628])
+        assert_allclose(windows.nuttall(7, True),
+                        [0.0003628, 0.0613345, 0.5292298, 1.0, 0.5292298,
+                         0.0613345, 0.0003628])
+
+
+class TestParzen:
+
+    def test_basic(self):
+        assert_allclose(windows.parzen(6),
+                        [0.009259259259259254, 0.25, 0.8611111111111112,
+                         0.8611111111111112, 0.25, 0.009259259259259254])
+        assert_allclose(windows.parzen(7, sym=True),
+                        [0.00583090379008747, 0.1574344023323616,
+                         0.6501457725947521, 1.0, 0.6501457725947521,
+                         0.1574344023323616, 0.00583090379008747])
+        assert_allclose(windows.parzen(6, False),
+                        [0.00583090379008747, 0.1574344023323616,
+                         0.6501457725947521, 1.0, 0.6501457725947521,
+                         0.1574344023323616])
+
+
+class TestTriang:
+
+    def test_basic(self):
+
+        assert_allclose(windows.triang(6, True),
+                        [1/6, 1/2, 5/6, 5/6, 1/2, 1/6])
+        assert_allclose(windows.triang(7),
+                        [1/4, 1/2, 3/4, 1, 3/4, 1/2, 1/4])
+        assert_allclose(windows.triang(6, sym=False),
+                        [1/4, 1/2, 3/4, 1, 3/4, 1/2])
+
+
+tukey_data = {
+    (4, 0.5, True): array([0.0, 1.0, 1.0, 0.0]),
+    (4, 0.9, True): array([0.0, 0.84312081893436686,
+                           0.84312081893436686, 0.0]),
+    (4, 1.0, True): array([0.0, 0.75, 0.75, 0.0]),
+    (4, 0.5, False): array([0.0, 1.0, 1.0, 1.0]),
+    (4, 0.9, False): array([0.0, 0.58682408883346526,
+                            1.0, 0.58682408883346526]),
+    (4, 1.0, False): array([0.0, 0.5, 1.0, 0.5]),
+    (5, 0.0, True): array([1.0, 1.0, 1.0, 1.0, 1.0]),
+    (5, 0.8, True): array([0.0, 0.69134171618254492,
+                           1.0, 0.69134171618254492, 0.0]),
+    (5, 1.0, True): array([0.0, 0.5, 1.0, 0.5, 0.0]),
+
+    (6, 0): [1, 1, 1, 1, 1, 1],
+    (7, 0): [1, 1, 1, 1, 1, 1, 1],
+    (6, .25): [0, 1, 1, 1, 1, 0],
+    (7, .25): [0, 1, 1, 1, 1, 1, 0],
+    (6,): [0, 0.9045084971874737, 1.0, 1.0, 0.9045084971874735, 0],
+    (7,): [0, 0.75, 1.0, 1.0, 1.0, 0.75, 0],
+    (6, .75): [0, 0.5522642316338269, 1.0, 1.0, 0.5522642316338267, 0],
+    (7, .75): [0, 0.4131759111665348, 0.9698463103929542, 1.0,
+               0.9698463103929542, 0.4131759111665347, 0],
+    (6, 1): [0, 0.3454915028125263, 0.9045084971874737, 0.9045084971874737,
+             0.3454915028125263, 0],
+    (7, 1): [0, 0.25, 0.75, 1.0, 0.75, 0.25, 0],
+}
+
+
+class TestTukey:
+
+    def test_basic(self):
+        # Test against hardcoded data
+        for k, v in tukey_data.items():
+            if v is None:
+                assert_raises(ValueError, windows.tukey, *k)
+            else:
+                win = windows.tukey(*k)
+                assert_allclose(win, v, rtol=1e-15, atol=1e-15)
+
+    def test_extremes(self):
+        # Test extremes of alpha correspond to boxcar and hann
+        tuk0 = windows.tukey(100, 0)
+        box0 = windows.boxcar(100)
+        assert_array_almost_equal(tuk0, box0)
+
+        tuk1 = windows.tukey(100, 1)
+        han1 = windows.hann(100)
+        assert_array_almost_equal(tuk1, han1)
+
+
+dpss_data = {
+    # All values from MATLAB:
+    # * taper[1] of (3, 1.4, 3) sign-flipped
+    # * taper[3] of (5, 1.5, 5) sign-flipped
+    (4, 0.1, 2): ([[0.497943898, 0.502047681, 0.502047681, 0.497943898], [0.670487993, 0.224601537, -0.224601537, -0.670487993]], [0.197961815, 0.002035474]),  # noqa: E501
+    (3, 1.4, 3): ([[0.410233151, 0.814504464, 0.410233151], [0.707106781, 0.0, -0.707106781], [0.575941629, -0.580157287, 0.575941629]], [0.999998093, 0.998067480, 0.801934426]),  # noqa: E501
+    (5, 1.5, 5): ([[0.1745071052, 0.4956749177, 0.669109327, 0.495674917, 0.174507105], [0.4399493348, 0.553574369, 0.0, -0.553574369, -0.439949334], [0.631452756, 0.073280238, -0.437943884, 0.073280238, 0.631452756], [0.553574369, -0.439949334, 0.0, 0.439949334, -0.553574369], [0.266110290, -0.498935248, 0.600414741, -0.498935248, 0.266110290147157]], [0.999728571, 0.983706916, 0.768457889, 0.234159338, 0.013947282907567]),  # noqa: E501
+    (100, 2, 4): ([[0.0030914414, 0.0041266922, 0.005315076, 0.006665149, 0.008184854, 0.0098814158, 0.011761239, 0.013829809, 0.016091597, 0.018549973, 0.02120712, 0.02406396, 0.027120092, 0.030373728, 0.033821651, 0.037459181, 0.041280145, 0.045276872, 0.049440192, 0.053759447, 0.058222524, 0.062815894, 0.067524661, 0.072332638, 0.077222418, 0.082175473, 0.087172252, 0.092192299, 0.097214376, 0.1022166, 0.10717657, 0.11207154, 0.11687856, 0.12157463, 0.12613686, 0.13054266, 0.13476986, 0.13879691, 0.14260302, 0.14616832, 0.14947401, 0.1525025, 0.15523755, 0.15766438, 0.15976981, 0.16154233, 0.16297223, 0.16405162, 0.16477455, 0.16513702, 0.16513702, 0.16477455, 0.16405162, 0.16297223, 0.16154233, 0.15976981, 0.15766438, 0.15523755, 0.1525025, 0.14947401, 0.14616832, 0.14260302, 0.13879691, 0.13476986, 0.13054266, 0.12613686, 0.12157463, 0.11687856, 0.11207154, 0.10717657, 0.1022166, 0.097214376, 0.092192299, 0.087172252, 0.082175473, 0.077222418, 0.072332638, 0.067524661, 0.062815894, 0.058222524, 0.053759447, 0.049440192, 0.045276872, 0.041280145, 0.037459181, 0.033821651, 0.030373728, 0.027120092, 0.02406396, 0.02120712, 0.018549973, 0.016091597, 0.013829809, 0.011761239, 0.0098814158, 0.008184854, 0.006665149, 0.005315076, 0.0041266922, 0.0030914414], [0.018064449, 0.022040342, 0.026325013, 0.030905288, 0.035764398, 0.040881982, 0.046234148, 0.051793558, 0.057529559, 0.063408356, 0.069393216, 0.075444716, 0.081521022, 0.087578202, 0.093570567, 0.099451049, 0.10517159, 0.11068356, 0.11593818, 0.12088699, 0.12548227, 0.12967752, 0.1334279, 0.13669069, 0.13942569, 0.1415957, 0.14316686, 0.14410905, 0.14439626, 0.14400686, 0.14292389, 0.1411353, 0.13863416, 0.13541876, 0.13149274, 0.12686516, 0.12155045, 0.1155684, 0.10894403, 0.10170748, 0.093893752, 0.08554251, 0.076697768, 0.067407559, 0.057723559, 0.04770068, 0.037396627, 0.026871428, 0.016186944, 0.0054063557, -0.0054063557, -0.016186944, -0.026871428, -0.037396627, -0.04770068, -0.057723559, -0.067407559, -0.076697768, -0.08554251, -0.093893752, -0.10170748, -0.10894403, -0.1155684, -0.12155045, -0.12686516, -0.13149274, -0.13541876, -0.13863416, -0.1411353, -0.14292389, -0.14400686, -0.14439626, -0.14410905, -0.14316686, -0.1415957, -0.13942569, -0.13669069, -0.1334279, -0.12967752, -0.12548227, -0.12088699, -0.11593818, -0.11068356, -0.10517159, -0.099451049, -0.093570567, -0.087578202, -0.081521022, -0.075444716, -0.069393216, -0.063408356, -0.057529559, -0.051793558, -0.046234148, -0.040881982, -0.035764398, -0.030905288, -0.026325013, -0.022040342, -0.018064449], [0.064817553, 0.072567801, 0.080292992, 0.087918235, 0.095367076, 0.10256232, 0.10942687, 0.1158846, 0.12186124, 0.12728523, 0.13208858, 0.13620771, 0.13958427, 0.14216587, 0.14390678, 0.14476863, 0.1447209, 0.14374148, 0.14181704, 0.13894336, 0.13512554, 0.13037812, 0.1247251, 0.11819984, 0.11084487, 0.10271159, 0.093859853, 0.084357497, 0.074279719, 0.063708406, 0.052731374, 0.041441525, 0.029935953, 0.018314987, 0.0066811877, -0.0048616765, -0.016209689, -0.027259848, -0.037911124, -0.048065512, -0.05762905, -0.066512804, -0.0746338, -0.081915903, -0.088290621, -0.09369783, -0.098086416, -0.10141482, -0.10365146, -0.10477512, -0.10477512, -0.10365146, -0.10141482, -0.098086416, -0.09369783, -0.088290621, -0.081915903, -0.0746338, -0.066512804, -0.05762905, -0.048065512, -0.037911124, -0.027259848, -0.016209689, -0.0048616765, 0.0066811877, 0.018314987, 0.029935953, 0.041441525, 0.052731374, 0.063708406, 0.074279719, 0.084357497, 0.093859853, 0.10271159, 0.11084487, 0.11819984, 0.1247251, 0.13037812, 0.13512554, 0.13894336, 0.14181704, 0.14374148, 0.1447209, 0.14476863, 0.14390678, 0.14216587, 0.13958427, 0.13620771, 0.13208858, 0.12728523, 0.12186124, 0.1158846, 0.10942687, 0.10256232, 0.095367076, 0.087918235, 0.080292992, 0.072567801, 0.064817553], [0.14985551, 0.15512305, 0.15931467, 0.16236806, 0.16423291, 0.16487165, 0.16426009, 0.1623879, 0.1592589, 0.15489114, 0.14931693, 0.14258255, 0.13474785, 0.1258857, 0.11608124, 0.10543095, 0.094041635, 0.082029213, 0.069517411, 0.056636348, 0.043521028, 0.030309756, 0.017142511, 0.0041592774, -0.0085016282, -0.020705223, -0.032321494, -0.043226982, -0.053306291, -0.062453515, -0.070573544, -0.077583253, -0.083412547, -0.088005244, -0.091319802, -0.093329861, -0.094024602, -0.093408915, -0.091503383, -0.08834406, -0.08398207, -0.078483012, -0.071926192, -0.064403681, -0.056019215, -0.046886954, -0.037130106, -0.026879442, -0.016271713, -0.005448, 0.005448, 0.016271713, 0.026879442, 0.037130106, 0.046886954, 0.056019215, 0.064403681, 0.071926192, 0.078483012, 0.08398207, 0.08834406, 0.091503383, 0.093408915, 0.094024602, 0.093329861, 0.091319802, 0.088005244, 0.083412547, 0.077583253, 0.070573544, 0.062453515, 0.053306291, 0.043226982, 0.032321494, 0.020705223, 0.0085016282, -0.0041592774, -0.017142511, -0.030309756, -0.043521028, -0.056636348, -0.069517411, -0.082029213, -0.094041635, -0.10543095, -0.11608124, -0.1258857, -0.13474785, -0.14258255, -0.14931693, -0.15489114, -0.1592589, -0.1623879, -0.16426009, -0.16487165, -0.16423291, -0.16236806, -0.15931467, -0.15512305, -0.14985551]], [0.999943140, 0.997571533, 0.959465463, 0.721862496]),  # noqa: E501
+}
+
+
+class TestDPSS:
+
+    def test_basic(self):
+        # Test against hardcoded data
+        for k, v in dpss_data.items():
+            win, ratios = windows.dpss(*k, return_ratios=True)
+            assert_allclose(win, v[0], atol=1e-7, err_msg=k)
+            assert_allclose(ratios, v[1], rtol=1e-5, atol=1e-7, err_msg=k)
+
+    def test_unity(self):
+        # Test unity value handling (gh-2221)
+        for M in range(1, 21):
+            # corrected w/approximation (default)
+            win = windows.dpss(M, M / 2.1)
+            expected = M % 2  # one for odd, none for even
+            assert_equal(np.isclose(win, 1.).sum(), expected,
+                         err_msg=f'{win}')
+            # corrected w/subsample delay (slower)
+            win_sub = windows.dpss(M, M / 2.1, norm='subsample')
+            if M > 2:
+                # @M=2 the subsample doesn't do anything
+                assert_equal(np.isclose(win_sub, 1.).sum(), expected,
+                             err_msg=f'{win_sub}')
+                assert_allclose(win, win_sub, rtol=0.03)  # within 3%
+            # not the same, l2-norm
+            win_2 = windows.dpss(M, M / 2.1, norm=2)
+            expected = 1 if M == 1 else 0
+            assert_equal(np.isclose(win_2, 1.).sum(), expected,
+                         err_msg=f'{win_2}')
+
+    def test_extremes(self):
+        # Test extremes of alpha
+        lam = windows.dpss(31, 6, 4, return_ratios=True)[1]
+        assert_array_almost_equal(lam, 1.)
+        lam = windows.dpss(31, 7, 4, return_ratios=True)[1]
+        assert_array_almost_equal(lam, 1.)
+        lam = windows.dpss(31, 8, 4, return_ratios=True)[1]
+        assert_array_almost_equal(lam, 1.)
+
+    def test_degenerate(self):
+        # Test failures
+        assert_raises(ValueError, windows.dpss, 4, 1.5, -1)  # Bad Kmax
+        assert_raises(ValueError, windows.dpss, 4, 1.5, -5)
+        assert_raises(TypeError, windows.dpss, 4, 1.5, 1.1)
+        assert_raises(ValueError, windows.dpss, 3, 1.5, 3)  # NW must be < N/2.
+        assert_raises(ValueError, windows.dpss, 3, -1, 3)  # NW must be pos
+        assert_raises(ValueError, windows.dpss, 3, 0, 3)
+        assert_raises(ValueError, windows.dpss, -1, 1, 3)  # negative M
+
+
+class TestLanczos:
+
+    def test_basic(self):
+        # Analytical results:
+        # sinc(x) = sinc(-x)
+        # sinc(pi) = 0, sinc(0) = 1
+        # Hand computation on WolframAlpha:
+        # sinc(2 pi / 3) = 0.413496672
+        # sinc(pi / 3) = 0.826993343
+        # sinc(3 pi / 5) = 0.504551152
+        # sinc(pi / 5) = 0.935489284
+        assert_allclose(windows.lanczos(6, sym=False),
+                        [0., 0.413496672,
+                         0.826993343, 1., 0.826993343,
+                         0.413496672],
+                        atol=1e-9)
+        assert_allclose(windows.lanczos(6),
+                        [0., 0.504551152,
+                         0.935489284, 0.935489284,
+                         0.504551152, 0.],
+                        atol=1e-9)
+        assert_allclose(windows.lanczos(7, sym=True),
+                        [0., 0.413496672,
+                         0.826993343, 1., 0.826993343,
+                         0.413496672, 0.],
+                        atol=1e-9)
+
+    def test_array_size(self):
+        for n in [0, 10, 11]:
+            assert_equal(len(windows.lanczos(n, sym=False)), n)
+            assert_equal(len(windows.lanczos(n, sym=True)), n)
+
+
+class TestGetWindow:
+
+    def test_boxcar(self):
+        w = windows.get_window('boxcar', 12)
+        assert_array_equal(w, np.ones_like(w))
+
+        # window is a tuple of len 1
+        w = windows.get_window(('boxcar',), 16)
+        assert_array_equal(w, np.ones_like(w))
+
+    def test_cheb_odd(self):
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning, "This window is not suitable")
+            w = windows.get_window(('chebwin', -40), 53, fftbins=False)
+        assert_array_almost_equal(w, cheb_odd_true, decimal=4)
+
+    def test_cheb_even(self):
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning, "This window is not suitable")
+            w = windows.get_window(('chebwin', 40), 54, fftbins=False)
+        assert_array_almost_equal(w, cheb_even_true, decimal=4)
+
+    def test_dpss(self):
+        win1 = windows.get_window(('dpss', 3), 64, fftbins=False)
+        win2 = windows.dpss(64, 3)
+        assert_array_almost_equal(win1, win2, decimal=4)
+
+    def test_kaiser_float(self):
+        win1 = windows.get_window(7.2, 64)
+        win2 = windows.kaiser(64, 7.2, False)
+        assert_allclose(win1, win2)
+
+    def test_invalid_inputs(self):
+        # Window is not a float, tuple, or string
+        assert_raises(ValueError, windows.get_window, set('hann'), 8)
+
+        # Unknown window type error
+        assert_raises(ValueError, windows.get_window, 'broken', 4)
+
+    def test_array_as_window(self):
+        # github issue 3603
+        osfactor = 128
+        sig = np.arange(128)
+
+        win = windows.get_window(('kaiser', 8.0), osfactor // 2)
+        with assert_raises(ValueError, match='must have the same length'):
+            resample(sig, len(sig) * osfactor, window=win)
+
+    def test_general_cosine(self):
+        assert_allclose(get_window(('general_cosine', [0.5, 0.3, 0.2]), 4),
+                        [0.4, 0.3, 1, 0.3])
+        assert_allclose(get_window(('general_cosine', [0.5, 0.3, 0.2]), 4,
+                                   fftbins=False),
+                        [0.4, 0.55, 0.55, 0.4])
+
+    def test_general_hamming(self):
+        assert_allclose(get_window(('general_hamming', 0.7), 5),
+                        [0.4, 0.6072949, 0.9427051, 0.9427051, 0.6072949])
+        assert_allclose(get_window(('general_hamming', 0.7), 5, fftbins=False),
+                        [0.4, 0.7, 1.0, 0.7, 0.4])
+
+    def test_lanczos(self):
+        assert_allclose(get_window('lanczos', 6),
+                        [0., 0.413496672, 0.826993343, 1., 0.826993343,
+                         0.413496672], atol=1e-9)
+        assert_allclose(get_window('lanczos', 6, fftbins=False),
+                        [0., 0.504551152, 0.935489284, 0.935489284,
+                         0.504551152, 0.], atol=1e-9)
+        assert_allclose(get_window('lanczos', 6), get_window('sinc', 6))
+
+
+def test_windowfunc_basics():
+    for window_name, params in window_funcs:
+        window = getattr(windows, window_name)
+        with suppress_warnings() as sup:
+            sup.filter(UserWarning, "This window is not suitable")
+            # Check symmetry for odd and even lengths
+            w1 = window(8, *params, sym=True)
+            w2 = window(7, *params, sym=False)
+            assert_array_almost_equal(w1[:-1], w2)
+
+            w1 = window(9, *params, sym=True)
+            w2 = window(8, *params, sym=False)
+            assert_array_almost_equal(w1[:-1], w2)
+
+            # Check that functions run and output lengths are correct
+            assert_equal(len(window(6, *params, sym=True)), 6)
+            assert_equal(len(window(6, *params, sym=False)), 6)
+            assert_equal(len(window(7, *params, sym=True)), 7)
+            assert_equal(len(window(7, *params, sym=False)), 7)
+
+            # Check invalid lengths
+            assert_raises(ValueError, window, 5.5, *params)
+            assert_raises(ValueError, window, -7, *params)
+
+            # Check degenerate cases
+            assert_array_equal(window(0, *params, sym=True), [])
+            assert_array_equal(window(0, *params, sym=False), [])
+            assert_array_equal(window(1, *params, sym=True), [1])
+            assert_array_equal(window(1, *params, sym=False), [1])
+
+            # Check dtype
+            assert_(window(0, *params, sym=True).dtype == 'float')
+            assert_(window(0, *params, sym=False).dtype == 'float')
+            assert_(window(1, *params, sym=True).dtype == 'float')
+            assert_(window(1, *params, sym=False).dtype == 'float')
+            assert_(window(6, *params, sym=True).dtype == 'float')
+            assert_(window(6, *params, sym=False).dtype == 'float')
+
+            # Check normalization
+            assert_array_less(window(10, *params, sym=True), 1.01)
+            assert_array_less(window(10, *params, sym=False), 1.01)
+            assert_array_less(window(9, *params, sym=True), 1.01)
+            assert_array_less(window(9, *params, sym=False), 1.01)
+
+            # Check that DFT-even spectrum is purely real for odd and even
+            assert_allclose(fft(window(10, *params, sym=False)).imag,
+                            0, atol=1e-14)
+            assert_allclose(fft(window(11, *params, sym=False)).imag,
+                            0, atol=1e-14)
+
+
+def test_needs_params():
+    for winstr in ['kaiser', 'ksr', 'kaiser_bessel_derived', 'kbd',
+                   'gaussian', 'gauss', 'gss',
+                   'general gaussian', 'general_gaussian',
+                   'general gauss', 'general_gauss', 'ggs',
+                   'dss', 'dpss', 'general cosine', 'general_cosine',
+                   'chebwin', 'cheb', 'general hamming', 'general_hamming',
+                   ]:
+        assert_raises(ValueError, get_window, winstr, 7)
+
+
+def test_not_needs_params():
+    for winstr in ['barthann',
+                   'bartlett',
+                   'blackman',
+                   'blackmanharris',
+                   'bohman',
+                   'boxcar',
+                   'cosine',
+                   'flattop',
+                   'hamming',
+                   'nuttall',
+                   'parzen',
+                   'taylor',
+                   'exponential',
+                   'poisson',
+                   'tukey',
+                   'tuk',
+                   'triangle',
+                   'lanczos',
+                   'sinc',
+                   ]:
+        win = get_window(winstr, 7)
+        assert_equal(len(win), 7)
+
+
+def test_symmetric():
+
+    for win in [windows.lanczos]:
+        # Even sampling points
+        w = win(4096)
+        error = np.max(np.abs(w-np.flip(w)))
+        assert_equal(error, 0.0)
+
+        # Odd sampling points
+        w = win(4097)
+        error = np.max(np.abs(w-np.flip(w)))
+        assert_equal(error, 0.0)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/__init__.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..967a7c758f69c1c8002d886d78832904c402d2b3
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/__init__.py
@@ -0,0 +1,52 @@
+"""
+Window functions (:mod:`scipy.signal.windows`)
+==============================================
+
+The suite of window functions for filtering and spectral estimation.
+
+.. currentmodule:: scipy.signal.windows
+
+.. autosummary::
+   :toctree: generated/
+
+   get_window              -- Return a window of a given length and type.
+
+   barthann                -- Bartlett-Hann window
+   bartlett                -- Bartlett window
+   blackman                -- Blackman window
+   blackmanharris          -- Minimum 4-term Blackman-Harris window
+   bohman                  -- Bohman window
+   boxcar                  -- Boxcar window
+   chebwin                 -- Dolph-Chebyshev window
+   cosine                  -- Cosine window
+   dpss                    -- Discrete prolate spheroidal sequences
+   exponential             -- Exponential window
+   flattop                 -- Flat top window
+   gaussian                -- Gaussian window
+   general_cosine          -- Generalized Cosine window
+   general_gaussian        -- Generalized Gaussian window
+   general_hamming         -- Generalized Hamming window
+   hamming                 -- Hamming window
+   hann                    -- Hann window
+   kaiser                  -- Kaiser window
+   kaiser_bessel_derived   -- Kaiser-Bessel derived window
+   lanczos                 -- Lanczos window also known as a sinc window
+   nuttall                 -- Nuttall's minimum 4-term Blackman-Harris window
+   parzen                  -- Parzen window
+   taylor                  -- Taylor window
+   triang                  -- Triangular window
+   tukey                   -- Tukey window
+
+"""
+
+from ._windows import *
+
+# Deprecated namespaces, to be removed in v2.0.0
+from . import windows
+
+__all__ = ['boxcar', 'triang', 'parzen', 'bohman', 'blackman', 'nuttall',
+           'blackmanharris', 'flattop', 'bartlett', 'barthann',
+           'hamming', 'kaiser', 'kaiser_bessel_derived', 'gaussian',
+           'general_gaussian', 'general_cosine', 'general_hamming',
+           'chebwin', 'cosine', 'hann', 'exponential', 'tukey', 'taylor',
+           'get_window', 'dpss', 'lanczos']
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/__pycache__/__init__.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ae805a3df3b78fbfc04a763899b514eb2d0da4aa
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/__pycache__/__init__.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/__pycache__/_windows.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/__pycache__/_windows.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d9154e313e17d95e1e3c4d044a8c2776a903fa00
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/__pycache__/_windows.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/__pycache__/windows.cpython-310.pyc b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/__pycache__/windows.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..37f410c2e29abf0cb7cb380ae64335b59a45265f
Binary files /dev/null and b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/__pycache__/windows.cpython-310.pyc differ
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/_windows.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/_windows.py
new file mode 100644
index 0000000000000000000000000000000000000000..bafd48b2457633c569dae640ca83158163820513
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/_windows.py
@@ -0,0 +1,2374 @@
+"""The suite of window functions."""
+
+import operator
+import warnings
+
+import numpy as np
+from scipy import linalg, special, fft as sp_fft
+
+__all__ = ['boxcar', 'triang', 'parzen', 'bohman', 'blackman', 'nuttall',
+           'blackmanharris', 'flattop', 'bartlett', 'barthann',
+           'hamming', 'kaiser', 'kaiser_bessel_derived', 'gaussian',
+           'general_cosine', 'general_gaussian', 'general_hamming',
+           'chebwin', 'cosine', 'hann', 'exponential', 'tukey', 'taylor',
+           'dpss', 'get_window', 'lanczos']
+
+
+def _len_guards(M):
+    """Handle small or incorrect window lengths"""
+    if int(M) != M or M < 0:
+        raise ValueError('Window length M must be a non-negative integer')
+    return M <= 1
+
+
+def _extend(M, sym):
+    """Extend window by 1 sample if needed for DFT-even symmetry"""
+    if not sym:
+        return M + 1, True
+    else:
+        return M, False
+
+
+def _truncate(w, needed):
+    """Truncate window by 1 sample if needed for DFT-even symmetry"""
+    if needed:
+        return w[:-1]
+    else:
+        return w
+
+
+def general_cosine(M, a, sym=True):
+    r"""
+    Generic weighted sum of cosine terms window
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window
+    a : array_like
+        Sequence of weighting coefficients. This uses the convention of being
+        centered on the origin, so these will typically all be positive
+        numbers, not alternating sign.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The array of window values.
+
+    References
+    ----------
+    .. [1] A. Nuttall, "Some windows with very good sidelobe behavior," IEEE
+           Transactions on Acoustics, Speech, and Signal Processing, vol. 29,
+           no. 1, pp. 84-91, Feb 1981. :doi:`10.1109/TASSP.1981.1163506`.
+    .. [2] Heinzel G. et al., "Spectrum and spectral density estimation by the
+           Discrete Fourier transform (DFT), including a comprehensive list of
+           window functions and some new flat-top windows", February 15, 2002
+           https://holometer.fnal.gov/GH_FFT.pdf
+
+    Examples
+    --------
+    Heinzel describes a flat-top window named "HFT90D" with formula: [2]_
+
+    .. math::  w_j = 1 - 1.942604 \cos(z) + 1.340318 \cos(2z)
+               - 0.440811 \cos(3z) + 0.043097 \cos(4z)
+
+    where
+
+    .. math::  z = \frac{2 \pi j}{N}, j = 0...N - 1
+
+    Since this uses the convention of starting at the origin, to reproduce the
+    window, we need to convert every other coefficient to a positive number:
+
+    >>> HFT90D = [1, 1.942604, 1.340318, 0.440811, 0.043097]
+
+    The paper states that the highest sidelobe is at -90.2 dB.  Reproduce
+    Figure 42 by plotting the window and its frequency response, and confirm
+    the sidelobe level in red:
+
+    >>> import numpy as np
+    >>> from scipy.signal.windows import general_cosine
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = general_cosine(1000, HFT90D, sym=False)
+    >>> plt.plot(window)
+    >>> plt.title("HFT90D window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 10000) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = np.abs(fftshift(A / abs(A).max()))
+    >>> response = 20 * np.log10(np.maximum(response, 1e-10))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-50/1000, 50/1000, -140, 0])
+    >>> plt.title("Frequency response of the HFT90D window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+    >>> plt.axhline(-90.2, color='red')
+    >>> plt.show()
+    """
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    fac = np.linspace(-np.pi, np.pi, M)
+    w = np.zeros(M)
+    for k in range(len(a)):
+        w += a[k] * np.cos(k * fac)
+
+    return _truncate(w, needs_trunc)
+
+
+def boxcar(M, sym=True):
+    """Return a boxcar or rectangular window.
+
+    Also known as a rectangular window or Dirichlet window, this is equivalent
+    to no window at all.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        Whether the window is symmetric. (Has no effect for boxcar.)
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1.
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.boxcar(51)
+    >>> plt.plot(window)
+    >>> plt.title("Boxcar window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the boxcar window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    w = np.ones(M, float)
+
+    return _truncate(w, needs_trunc)
+
+
+def triang(M, sym=True):
+    """Return a triangular window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    See Also
+    --------
+    bartlett : A triangular window that touches zero
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.triang(51)
+    >>> plt.plot(window)
+    >>> plt.title("Triangular window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = np.abs(fftshift(A / abs(A).max()))
+    >>> response = 20 * np.log10(np.maximum(response, 1e-10))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the triangular window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    n = np.arange(1, (M + 1) // 2 + 1)
+    if M % 2 == 0:
+        w = (2 * n - 1.0) / M
+        w = np.r_[w, w[::-1]]
+    else:
+        w = 2 * n / (M + 1.0)
+        w = np.r_[w, w[-2::-1]]
+
+    return _truncate(w, needs_trunc)
+
+
+def parzen(M, sym=True):
+    """Return a Parzen window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    References
+    ----------
+    .. [1] E. Parzen, "Mathematical Considerations in the Estimation of
+           Spectra", Technometrics,  Vol. 3, No. 2 (May, 1961), pp. 167-190
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.parzen(51)
+    >>> plt.plot(window)
+    >>> plt.title("Parzen window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the Parzen window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    n = np.arange(-(M - 1) / 2.0, (M - 1) / 2.0 + 0.5, 1.0)
+    na = np.extract(n < -(M - 1) / 4.0, n)
+    nb = np.extract(abs(n) <= (M - 1) / 4.0, n)
+    wa = 2 * (1 - np.abs(na) / (M / 2.0)) ** 3.0
+    wb = (1 - 6 * (np.abs(nb) / (M / 2.0)) ** 2.0 +
+          6 * (np.abs(nb) / (M / 2.0)) ** 3.0)
+    w = np.r_[wa, wb, wa[::-1]]
+
+    return _truncate(w, needs_trunc)
+
+
+def bohman(M, sym=True):
+    """Return a Bohman window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.bohman(51)
+    >>> plt.plot(window)
+    >>> plt.title("Bohman window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2047) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the Bohman window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    fac = np.abs(np.linspace(-1, 1, M)[1:-1])
+    w = (1 - fac) * np.cos(np.pi * fac) + 1.0 / np.pi * np.sin(np.pi * fac)
+    w = np.r_[0, w, 0]
+
+    return _truncate(w, needs_trunc)
+
+
+def blackman(M, sym=True):
+    r"""
+    Return a Blackman window.
+
+    The Blackman window is a taper formed by using the first three terms of
+    a summation of cosines. It was designed to have close to the minimal
+    leakage possible.  It is close to optimal, only slightly worse than a
+    Kaiser window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Notes
+    -----
+    The Blackman window is defined as
+
+    .. math::  w(n) = 0.42 - 0.5 \cos(2\pi n/M) + 0.08 \cos(4\pi n/M)
+
+    The "exact Blackman" window was designed to null out the third and fourth
+    sidelobes, but has discontinuities at the boundaries, resulting in a
+    6 dB/oct fall-off.  This window is an approximation of the "exact" window,
+    which does not null the sidelobes as well, but is smooth at the edges,
+    improving the fall-off rate to 18 dB/oct. [3]_
+
+    Most references to the Blackman window come from the signal processing
+    literature, where it is used as one of many windowing functions for
+    smoothing values.  It is also known as an apodization (which means
+    "removing the foot", i.e. smoothing discontinuities at the beginning
+    and end of the sampled signal) or tapering function. It is known as a
+    "near optimal" tapering function, almost as good (by some measures)
+    as the Kaiser window.
+
+    References
+    ----------
+    .. [1] Blackman, R.B. and Tukey, J.W., (1958) The measurement of power
+           spectra, Dover Publications, New York.
+    .. [2] Oppenheim, A.V., and R.W. Schafer. Discrete-Time Signal Processing.
+           Upper Saddle River, NJ: Prentice-Hall, 1999, pp. 468-471.
+    .. [3] Harris, Fredric J. (Jan 1978). "On the use of Windows for Harmonic
+           Analysis with the Discrete Fourier Transform". Proceedings of the
+           IEEE 66 (1): 51-83. :doi:`10.1109/PROC.1978.10837`.
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.blackman(51)
+    >>> plt.plot(window)
+    >>> plt.title("Blackman window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = np.abs(fftshift(A / abs(A).max()))
+    >>> response = 20 * np.log10(np.maximum(response, 1e-10))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the Blackman window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    # Docstring adapted from NumPy's blackman function
+    return general_cosine(M, [0.42, 0.50, 0.08], sym)
+
+
+def nuttall(M, sym=True):
+    """Return a minimum 4-term Blackman-Harris window according to Nuttall.
+
+    This variation is called "Nuttall4c" by Heinzel. [2]_
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    References
+    ----------
+    .. [1] A. Nuttall, "Some windows with very good sidelobe behavior," IEEE
+           Transactions on Acoustics, Speech, and Signal Processing, vol. 29,
+           no. 1, pp. 84-91, Feb 1981. :doi:`10.1109/TASSP.1981.1163506`.
+    .. [2] Heinzel G. et al., "Spectrum and spectral density estimation by the
+           Discrete Fourier transform (DFT), including a comprehensive list of
+           window functions and some new flat-top windows", February 15, 2002
+           https://holometer.fnal.gov/GH_FFT.pdf
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.nuttall(51)
+    >>> plt.plot(window)
+    >>> plt.title("Nuttall window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the Nuttall window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    return general_cosine(M, [0.3635819, 0.4891775, 0.1365995, 0.0106411], sym)
+
+
+def blackmanharris(M, sym=True):
+    """Return a minimum 4-term Blackman-Harris window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.blackmanharris(51)
+    >>> plt.plot(window)
+    >>> plt.title("Blackman-Harris window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the Blackman-Harris window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    return general_cosine(M, [0.35875, 0.48829, 0.14128, 0.01168], sym)
+
+
+def flattop(M, sym=True):
+    """Return a flat top window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Notes
+    -----
+    Flat top windows are used for taking accurate measurements of signal
+    amplitude in the frequency domain, with minimal scalloping error from the
+    center of a frequency bin to its edges, compared to others.  This is a
+    5th-order cosine window, with the 5 terms optimized to make the main lobe
+    maximally flat. [1]_
+
+    References
+    ----------
+    .. [1] D'Antona, Gabriele, and A. Ferrero, "Digital Signal Processing for
+           Measurement Systems", Springer Media, 2006, p. 70
+           :doi:`10.1007/0-387-28666-7`.
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.flattop(51)
+    >>> plt.plot(window)
+    >>> plt.title("Flat top window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the flat top window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    a = [0.21557895, 0.41663158, 0.277263158, 0.083578947, 0.006947368]
+    return general_cosine(M, a, sym)
+
+
+def bartlett(M, sym=True):
+    r"""
+    Return a Bartlett window.
+
+    The Bartlett window is very similar to a triangular window, except
+    that the end points are at zero.  It is often used in signal
+    processing for tapering a signal, without generating too much
+    ripple in the frequency domain.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The triangular window, with the first and last samples equal to zero
+        and the maximum value normalized to 1 (though the value 1 does not
+        appear if `M` is even and `sym` is True).
+
+    See Also
+    --------
+    triang : A triangular window that does not touch zero at the ends
+
+    Notes
+    -----
+    The Bartlett window is defined as
+
+    .. math:: w(n) = \frac{2}{M-1} \left(
+              \frac{M-1}{2} - \left|n - \frac{M-1}{2}\right|
+              \right)
+
+    Most references to the Bartlett window come from the signal
+    processing literature, where it is used as one of many windowing
+    functions for smoothing values.  Note that convolution with this
+    window produces linear interpolation.  It is also known as an
+    apodization (which means"removing the foot", i.e. smoothing
+    discontinuities at the beginning and end of the sampled signal) or
+    tapering function. The Fourier transform of the Bartlett is the product
+    of two sinc functions.
+    Note the excellent discussion in Kanasewich. [2]_
+
+    References
+    ----------
+    .. [1] M.S. Bartlett, "Periodogram Analysis and Continuous Spectra",
+           Biometrika 37, 1-16, 1950.
+    .. [2] E.R. Kanasewich, "Time Sequence Analysis in Geophysics",
+           The University of Alberta Press, 1975, pp. 109-110.
+    .. [3] A.V. Oppenheim and R.W. Schafer, "Discrete-Time Signal
+           Processing", Prentice-Hall, 1999, pp. 468-471.
+    .. [4] Wikipedia, "Window function",
+           https://en.wikipedia.org/wiki/Window_function
+    .. [5] W.H. Press,  B.P. Flannery, S.A. Teukolsky, and W.T. Vetterling,
+           "Numerical Recipes", Cambridge University Press, 1986, page 429.
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.bartlett(51)
+    >>> plt.plot(window)
+    >>> plt.title("Bartlett window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the Bartlett window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    # Docstring adapted from NumPy's bartlett function
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    n = np.arange(0, M)
+    w = np.where(np.less_equal(n, (M - 1) / 2.0),
+                 2.0 * n / (M - 1), 2.0 - 2.0 * n / (M - 1))
+
+    return _truncate(w, needs_trunc)
+
+
+def hann(M, sym=True):
+    r"""
+    Return a Hann window.
+
+    The Hann window is a taper formed by using a raised cosine or sine-squared
+    with ends that touch zero.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Notes
+    -----
+    The Hann window is defined as
+
+    .. math::  w(n) = 0.5 - 0.5 \cos\left(\frac{2\pi{n}}{M-1}\right)
+               \qquad 0 \leq n \leq M-1
+
+    The window was named for Julius von Hann, an Austrian meteorologist. It is
+    also known as the Cosine Bell. It is sometimes erroneously referred to as
+    the "Hanning" window, from the use of "hann" as a verb in the original
+    paper and confusion with the very similar Hamming window.
+
+    Most references to the Hann window come from the signal processing
+    literature, where it is used as one of many windowing functions for
+    smoothing values.  It is also known as an apodization (which means
+    "removing the foot", i.e. smoothing discontinuities at the beginning
+    and end of the sampled signal) or tapering function.
+
+    References
+    ----------
+    .. [1] Blackman, R.B. and Tukey, J.W., (1958) The measurement of power
+           spectra, Dover Publications, New York.
+    .. [2] E.R. Kanasewich, "Time Sequence Analysis in Geophysics",
+           The University of Alberta Press, 1975, pp. 106-108.
+    .. [3] Wikipedia, "Window function",
+           https://en.wikipedia.org/wiki/Window_function
+    .. [4] W.H. Press,  B.P. Flannery, S.A. Teukolsky, and W.T. Vetterling,
+           "Numerical Recipes", Cambridge University Press, 1986, page 425.
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.hann(51)
+    >>> plt.plot(window)
+    >>> plt.title("Hann window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = np.abs(fftshift(A / abs(A).max()))
+    >>> response = 20 * np.log10(np.maximum(response, 1e-10))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the Hann window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    # Docstring adapted from NumPy's hanning function
+    return general_hamming(M, 0.5, sym)
+
+
+def tukey(M, alpha=0.5, sym=True):
+    r"""Return a Tukey window, also known as a tapered cosine window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    alpha : float, optional
+        Shape parameter of the Tukey window, representing the fraction of the
+        window inside the cosine tapered region.
+        If zero, the Tukey window is equivalent to a rectangular window.
+        If one, the Tukey window is equivalent to a Hann window.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    References
+    ----------
+    .. [1] Harris, Fredric J. (Jan 1978). "On the use of Windows for Harmonic
+           Analysis with the Discrete Fourier Transform". Proceedings of the
+           IEEE 66 (1): 51-83. :doi:`10.1109/PROC.1978.10837`
+    .. [2] Wikipedia, "Window function",
+           https://en.wikipedia.org/wiki/Window_function#Tukey_window
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.tukey(51)
+    >>> plt.plot(window)
+    >>> plt.title("Tukey window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+    >>> plt.ylim([0, 1.1])
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the Tukey window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    if _len_guards(M):
+        return np.ones(M)
+
+    if alpha <= 0:
+        return np.ones(M, 'd')
+    elif alpha >= 1.0:
+        return hann(M, sym=sym)
+
+    M, needs_trunc = _extend(M, sym)
+
+    n = np.arange(0, M)
+    width = int(np.floor(alpha*(M-1)/2.0))
+    n1 = n[0:width+1]
+    n2 = n[width+1:M-width-1]
+    n3 = n[M-width-1:]
+
+    w1 = 0.5 * (1 + np.cos(np.pi * (-1 + 2.0*n1/alpha/(M-1))))
+    w2 = np.ones(n2.shape)
+    w3 = 0.5 * (1 + np.cos(np.pi * (-2.0/alpha + 1 + 2.0*n3/alpha/(M-1))))
+
+    w = np.concatenate((w1, w2, w3))
+
+    return _truncate(w, needs_trunc)
+
+
+def barthann(M, sym=True):
+    """Return a modified Bartlett-Hann window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.barthann(51)
+    >>> plt.plot(window)
+    >>> plt.title("Bartlett-Hann window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the Bartlett-Hann window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    n = np.arange(0, M)
+    fac = np.abs(n / (M - 1.0) - 0.5)
+    w = 0.62 - 0.48 * fac + 0.38 * np.cos(2 * np.pi * fac)
+
+    return _truncate(w, needs_trunc)
+
+
+def general_hamming(M, alpha, sym=True):
+    r"""Return a generalized Hamming window.
+
+    The generalized Hamming window is constructed by multiplying a rectangular
+    window by one period of a cosine function [1]_.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    alpha : float
+        The window coefficient, :math:`\alpha`
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    See Also
+    --------
+    hamming, hann
+
+    Notes
+    -----
+    The generalized Hamming window is defined as
+
+    .. math:: w(n) = \alpha - \left(1 - \alpha\right)
+              \cos\left(\frac{2\pi{n}}{M-1}\right) \qquad 0 \leq n \leq M-1
+
+    Both the common Hamming window and Hann window are special cases of the
+    generalized Hamming window with :math:`\alpha` = 0.54 and :math:`\alpha` =
+    0.5, respectively [2]_.
+
+    References
+    ----------
+    .. [1] DSPRelated, "Generalized Hamming Window Family",
+           https://www.dsprelated.com/freebooks/sasp/Generalized_Hamming_Window_Family.html
+    .. [2] Wikipedia, "Window function",
+           https://en.wikipedia.org/wiki/Window_function
+    .. [3] Riccardo Piantanida ESA, "Sentinel-1 Level 1 Detailed Algorithm
+           Definition",
+           https://sentinel.esa.int/documents/247904/1877131/Sentinel-1-Level-1-Detailed-Algorithm-Definition
+    .. [4] Matthieu Bourbigot ESA, "Sentinel-1 Product Definition",
+           https://sentinel.esa.int/documents/247904/1877131/Sentinel-1-Product-Definition
+
+    Examples
+    --------
+    The Sentinel-1A/B Instrument Processing Facility uses generalized Hamming
+    windows in the processing of spaceborne Synthetic Aperture Radar (SAR)
+    data [3]_. The facility uses various values for the :math:`\alpha`
+    parameter based on operating mode of the SAR instrument. Some common
+    :math:`\alpha` values include 0.75, 0.7 and 0.52 [4]_. As an example, we
+    plot these different windows.
+
+    >>> import numpy as np
+    >>> from scipy.signal.windows import general_hamming
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> fig1, spatial_plot = plt.subplots()
+    >>> spatial_plot.set_title("Generalized Hamming Windows")
+    >>> spatial_plot.set_ylabel("Amplitude")
+    >>> spatial_plot.set_xlabel("Sample")
+
+    >>> fig2, freq_plot = plt.subplots()
+    >>> freq_plot.set_title("Frequency Responses")
+    >>> freq_plot.set_ylabel("Normalized magnitude [dB]")
+    >>> freq_plot.set_xlabel("Normalized frequency [cycles per sample]")
+
+    >>> for alpha in [0.75, 0.7, 0.52]:
+    ...     window = general_hamming(41, alpha)
+    ...     spatial_plot.plot(window, label="{:.2f}".format(alpha))
+    ...     A = fft(window, 2048) / (len(window)/2.0)
+    ...     freq = np.linspace(-0.5, 0.5, len(A))
+    ...     response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    ...     freq_plot.plot(freq, response, label="{:.2f}".format(alpha))
+    >>> freq_plot.legend(loc="upper right")
+    >>> spatial_plot.legend(loc="upper right")
+
+    """
+    return general_cosine(M, [alpha, 1. - alpha], sym)
+
+
+def hamming(M, sym=True):
+    r"""Return a Hamming window.
+
+    The Hamming window is a taper formed by using a raised cosine with
+    non-zero endpoints, optimized to minimize the nearest side lobe.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Notes
+    -----
+    The Hamming window is defined as
+
+    .. math::  w(n) = 0.54 - 0.46 \cos\left(\frac{2\pi{n}}{M-1}\right)
+               \qquad 0 \leq n \leq M-1
+
+    The Hamming was named for R. W. Hamming, an associate of J. W. Tukey and
+    is described in Blackman and Tukey. It was recommended for smoothing the
+    truncated autocovariance function in the time domain.
+    Most references to the Hamming window come from the signal processing
+    literature, where it is used as one of many windowing functions for
+    smoothing values.  It is also known as an apodization (which means
+    "removing the foot", i.e. smoothing discontinuities at the beginning
+    and end of the sampled signal) or tapering function.
+
+    References
+    ----------
+    .. [1] Blackman, R.B. and Tukey, J.W., (1958) The measurement of power
+           spectra, Dover Publications, New York.
+    .. [2] E.R. Kanasewich, "Time Sequence Analysis in Geophysics", The
+           University of Alberta Press, 1975, pp. 109-110.
+    .. [3] Wikipedia, "Window function",
+           https://en.wikipedia.org/wiki/Window_function
+    .. [4] W.H. Press,  B.P. Flannery, S.A. Teukolsky, and W.T. Vetterling,
+           "Numerical Recipes", Cambridge University Press, 1986, page 425.
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.hamming(51)
+    >>> plt.plot(window)
+    >>> plt.title("Hamming window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the Hamming window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    # Docstring adapted from NumPy's hamming function
+    return general_hamming(M, 0.54, sym)
+
+
+def kaiser(M, beta, sym=True):
+    r"""Return a Kaiser window.
+
+    The Kaiser window is a taper formed by using a Bessel function.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    beta : float
+        Shape parameter, determines trade-off between main-lobe width and
+        side lobe level. As beta gets large, the window narrows.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Notes
+    -----
+    The Kaiser window is defined as
+
+    .. math::  w(n) = I_0\left( \beta \sqrt{1-\frac{4n^2}{(M-1)^2}}
+               \right)/I_0(\beta)
+
+    with
+
+    .. math:: \quad -\frac{M-1}{2} \leq n \leq \frac{M-1}{2},
+
+    where :math:`I_0` is the modified zeroth-order Bessel function.
+
+    The Kaiser was named for Jim Kaiser, who discovered a simple approximation
+    to the DPSS window based on Bessel functions.
+    The Kaiser window is a very good approximation to the Digital Prolate
+    Spheroidal Sequence, or Slepian window, which is the transform which
+    maximizes the energy in the main lobe of the window relative to total
+    energy.
+
+    The Kaiser can approximate other windows by varying the beta parameter.
+    (Some literature uses alpha = beta/pi.) [4]_
+
+    ====  =======================
+    beta  Window shape
+    ====  =======================
+    0     Rectangular
+    5     Similar to a Hamming
+    6     Similar to a Hann
+    8.6   Similar to a Blackman
+    ====  =======================
+
+    A beta value of 14 is probably a good starting point. Note that as beta
+    gets large, the window narrows, and so the number of samples needs to be
+    large enough to sample the increasingly narrow spike, otherwise NaNs will
+    be returned.
+
+    Most references to the Kaiser window come from the signal processing
+    literature, where it is used as one of many windowing functions for
+    smoothing values.  It is also known as an apodization (which means
+    "removing the foot", i.e. smoothing discontinuities at the beginning
+    and end of the sampled signal) or tapering function.
+
+    References
+    ----------
+    .. [1] J. F. Kaiser, "Digital Filters" - Ch 7 in "Systems analysis by
+           digital computer", Editors: F.F. Kuo and J.F. Kaiser, p 218-285.
+           John Wiley and Sons, New York, (1966).
+    .. [2] E.R. Kanasewich, "Time Sequence Analysis in Geophysics", The
+           University of Alberta Press, 1975, pp. 177-178.
+    .. [3] Wikipedia, "Window function",
+           https://en.wikipedia.org/wiki/Window_function
+    .. [4] F. J. Harris, "On the use of windows for harmonic analysis with the
+           discrete Fourier transform," Proceedings of the IEEE, vol. 66,
+           no. 1, pp. 51-83, Jan. 1978. :doi:`10.1109/PROC.1978.10837`.
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.kaiser(51, beta=14)
+    >>> plt.plot(window)
+    >>> plt.title(r"Kaiser window ($\beta$=14)")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title(r"Frequency response of the Kaiser window ($\beta$=14)")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    # Docstring adapted from NumPy's kaiser function
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    n = np.arange(0, M)
+    alpha = (M - 1) / 2.0
+    w = (special.i0(beta * np.sqrt(1 - ((n - alpha) / alpha) ** 2.0)) /
+         special.i0(beta))
+
+    return _truncate(w, needs_trunc)
+
+
+def kaiser_bessel_derived(M, beta, *, sym=True):
+    """Return a Kaiser-Bessel derived window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+        Note that this window is only defined for an even
+        number of points.
+    beta : float
+        Kaiser window shape parameter.
+    sym : bool, optional
+        This parameter only exists to comply with the interface offered by
+        the other window functions and to be callable by `get_window`.
+        When True (default), generates a symmetric window, for use in filter
+        design.
+
+    Returns
+    -------
+    w : ndarray
+        The window, normalized to fulfil the Princen-Bradley condition.
+
+    See Also
+    --------
+    kaiser
+
+    Notes
+    -----
+    It is designed to be suitable for use with the modified discrete cosine
+    transform (MDCT) and is mainly used in audio signal processing and
+    audio coding.
+
+    .. versionadded:: 1.9.0
+
+    References
+    ----------
+    .. [1] Bosi, Marina, and Richard E. Goldberg. Introduction to Digital
+           Audio Coding and Standards. Dordrecht: Kluwer, 2003.
+    .. [2] Wikipedia, "Kaiser window",
+           https://en.wikipedia.org/wiki/Kaiser_window
+
+    Examples
+    --------
+    Plot the Kaiser-Bessel derived window based on the wikipedia
+    reference [2]_:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> import matplotlib.pyplot as plt
+    >>> fig, ax = plt.subplots()
+    >>> N = 50
+    >>> for alpha in [0.64, 2.55, 7.64, 31.83]:
+    ...     ax.plot(signal.windows.kaiser_bessel_derived(2*N, np.pi*alpha),
+    ...             label=f"{alpha=}")
+    >>> ax.grid(True)
+    >>> ax.set_title("Kaiser-Bessel derived window")
+    >>> ax.set_ylabel("Amplitude")
+    >>> ax.set_xlabel("Sample")
+    >>> ax.set_xticks([0, N, 2*N-1])
+    >>> ax.set_xticklabels(["0", "N", "2N+1"])  # doctest: +SKIP
+    >>> ax.set_yticks([0.0, 0.2, 0.4, 0.6, 0.707, 0.8, 1.0])
+    >>> fig.legend(loc="center")
+    >>> fig.tight_layout()
+    >>> fig.show()
+    """
+    if not sym:
+        raise ValueError(
+            "Kaiser-Bessel Derived windows are only defined for symmetric "
+            "shapes"
+        )
+    elif M < 1:
+        return np.array([])
+    elif M % 2:
+        raise ValueError(
+            "Kaiser-Bessel Derived windows are only defined for even number "
+            "of points"
+        )
+
+    kaiser_window = kaiser(M // 2 + 1, beta)
+    csum = np.cumsum(kaiser_window)
+    half_window = np.sqrt(csum[:-1] / csum[-1])
+    w = np.concatenate((half_window, half_window[::-1]), axis=0)
+    return w
+
+
+def gaussian(M, std, sym=True):
+    r"""Return a Gaussian window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    std : float
+        The standard deviation, sigma.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Notes
+    -----
+    The Gaussian window is defined as
+
+    .. math::  w(n) = e^{ -\frac{1}{2}\left(\frac{n}{\sigma}\right)^2 }
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.gaussian(51, std=7)
+    >>> plt.plot(window)
+    >>> plt.title(r"Gaussian window ($\sigma$=7)")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title(r"Frequency response of the Gaussian window ($\sigma$=7)")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    n = np.arange(0, M) - (M - 1.0) / 2.0
+    sig2 = 2 * std * std
+    w = np.exp(-n ** 2 / sig2)
+
+    return _truncate(w, needs_trunc)
+
+
+def general_gaussian(M, p, sig, sym=True):
+    r"""Return a window with a generalized Gaussian shape.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    p : float
+        Shape parameter.  p = 1 is identical to `gaussian`, p = 0.5 is
+        the same shape as the Laplace distribution.
+    sig : float
+        The standard deviation, sigma.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Notes
+    -----
+    The generalized Gaussian window is defined as
+
+    .. math::  w(n) = e^{ -\frac{1}{2}\left|\frac{n}{\sigma}\right|^{2p} }
+
+    the half-power point is at
+
+    .. math::  (2 \log(2))^{1/(2 p)} \sigma
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.general_gaussian(51, p=1.5, sig=7)
+    >>> plt.plot(window)
+    >>> plt.title(r"Generalized Gaussian window (p=1.5, $\sigma$=7)")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title(r"Freq. resp. of the gen. Gaussian "
+    ...           r"window (p=1.5, $\sigma$=7)")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    n = np.arange(0, M) - (M - 1.0) / 2.0
+    w = np.exp(-0.5 * np.abs(n / sig) ** (2 * p))
+
+    return _truncate(w, needs_trunc)
+
+
+# `chebwin` contributed by Kumar Appaiah.
+def chebwin(M, at, sym=True):
+    r"""Return a Dolph-Chebyshev window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    at : float
+        Attenuation (in dB).
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value always normalized to 1
+
+    Notes
+    -----
+    This window optimizes for the narrowest main lobe width for a given order
+    `M` and sidelobe equiripple attenuation `at`, using Chebyshev
+    polynomials.  It was originally developed by Dolph to optimize the
+    directionality of radio antenna arrays.
+
+    Unlike most windows, the Dolph-Chebyshev is defined in terms of its
+    frequency response:
+
+    .. math:: W(k) = \frac
+              {\cos\{M \cos^{-1}[\beta \cos(\frac{\pi k}{M})]\}}
+              {\cosh[M \cosh^{-1}(\beta)]}
+
+    where
+
+    .. math:: \beta = \cosh \left [\frac{1}{M}
+              \cosh^{-1}(10^\frac{A}{20}) \right ]
+
+    and 0 <= abs(k) <= M-1. A is the attenuation in decibels (`at`).
+
+    The time domain window is then generated using the IFFT, so
+    power-of-two `M` are the fastest to generate, and prime number `M` are
+    the slowest.
+
+    The equiripple condition in the frequency domain creates impulses in the
+    time domain, which appear at the ends of the window.
+
+    References
+    ----------
+    .. [1] C. Dolph, "A current distribution for broadside arrays which
+           optimizes the relationship between beam width and side-lobe level",
+           Proceedings of the IEEE, Vol. 34, Issue 6
+    .. [2] Peter Lynch, "The Dolph-Chebyshev Window: A Simple Optimal Filter",
+           American Meteorological Society (April 1997)
+           http://mathsci.ucd.ie/~plynch/Publications/Dolph.pdf
+    .. [3] F. J. Harris, "On the use of windows for harmonic analysis with the
+           discrete Fourier transforms", Proceedings of the IEEE, Vol. 66,
+           No. 1, January 1978
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.chebwin(51, at=100)
+    >>> plt.plot(window)
+    >>> plt.title("Dolph-Chebyshev window (100 dB)")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the Dolph-Chebyshev window (100 dB)")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """
+    if np.abs(at) < 45:
+        warnings.warn("This window is not suitable for spectral analysis "
+                      "for attenuation values lower than about 45dB because "
+                      "the equivalent noise bandwidth of a Chebyshev window "
+                      "does not grow monotonically with increasing sidelobe "
+                      "attenuation when the attenuation is smaller than "
+                      "about 45 dB.",
+                      stacklevel=2)
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    # compute the parameter beta
+    order = M - 1.0
+    beta = np.cosh(1.0 / order * np.arccosh(10 ** (np.abs(at) / 20.)))
+    k = np.r_[0:M] * 1.0
+    x = beta * np.cos(np.pi * k / M)
+    # Find the window's DFT coefficients
+    # Use analytic definition of Chebyshev polynomial instead of expansion
+    # from scipy.special. Using the expansion in scipy.special leads to errors.
+    p = np.zeros(x.shape)
+    p[x > 1] = np.cosh(order * np.arccosh(x[x > 1]))
+    p[x < -1] = (2 * (M % 2) - 1) * np.cosh(order * np.arccosh(-x[x < -1]))
+    p[np.abs(x) <= 1] = np.cos(order * np.arccos(x[np.abs(x) <= 1]))
+
+    # Appropriate IDFT and filling up
+    # depending on even/odd M
+    if M % 2:
+        w = np.real(sp_fft.fft(p))
+        n = (M + 1) // 2
+        w = w[:n]
+        w = np.concatenate((w[n - 1:0:-1], w))
+    else:
+        p = p * np.exp(1.j * np.pi / M * np.r_[0:M])
+        w = np.real(sp_fft.fft(p))
+        n = M // 2 + 1
+        w = np.concatenate((w[n - 1:0:-1], w[1:n]))
+    w = w / max(w)
+
+    return _truncate(w, needs_trunc)
+
+
+def cosine(M, sym=True):
+    """Return a window with a simple cosine shape.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Notes
+    -----
+
+    .. versionadded:: 0.13.0
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.cosine(51)
+    >>> plt.plot(window)
+    >>> plt.title("Cosine window")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2047) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the cosine window")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+    >>> plt.show()
+
+    """
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    w = np.sin(np.pi / M * (np.arange(0, M) + .5))
+
+    return _truncate(w, needs_trunc)
+
+
+def exponential(M, center=None, tau=1., sym=True):
+    r"""Return an exponential (or Poisson) window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    center : float, optional
+        Parameter defining the center location of the window function.
+        The default value if not given is ``center = (M-1) / 2``.  This
+        parameter must take its default value for symmetric windows.
+    tau : float, optional
+        Parameter defining the decay.  For ``center = 0`` use
+        ``tau = -(M-1) / ln(x)`` if ``x`` is the fraction of the window
+        remaining at the end.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Notes
+    -----
+    The Exponential window is defined as
+
+    .. math::  w(n) = e^{-|n-center| / \tau}
+
+    References
+    ----------
+    .. [1] S. Gade and H. Herlufsen, "Windows to FFT analysis (Part I)",
+           Technical Review 3, Bruel & Kjaer, 1987.
+
+    Examples
+    --------
+    Plot the symmetric window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> M = 51
+    >>> tau = 3.0
+    >>> window = signal.windows.exponential(M, tau=tau)
+    >>> plt.plot(window)
+    >>> plt.title("Exponential Window (tau=3.0)")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -35, 0])
+    >>> plt.title("Frequency response of the Exponential window (tau=3.0)")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    This function can also generate non-symmetric windows:
+
+    >>> tau2 = -(M-1) / np.log(0.01)
+    >>> window2 = signal.windows.exponential(M, 0, tau2, False)
+    >>> plt.figure()
+    >>> plt.plot(window2)
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+    """
+    if sym and center is not None:
+        raise ValueError("If sym==True, center must be None.")
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    if center is None:
+        center = (M-1) / 2
+
+    n = np.arange(0, M)
+    w = np.exp(-np.abs(n-center) / tau)
+
+    return _truncate(w, needs_trunc)
+
+
+def taylor(M, nbar=4, sll=30, norm=True, sym=True):
+    """
+    Return a Taylor window.
+
+    The Taylor window taper function approximates the Dolph-Chebyshev window's
+    constant sidelobe level for a parameterized number of near-in sidelobes,
+    but then allows a taper beyond [2]_.
+
+    The SAR (synthetic aperture radar) community commonly uses Taylor
+    weighting for image formation processing because it provides strong,
+    selectable sidelobe suppression with minimum broadening of the
+    mainlobe [1]_.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    nbar : int, optional
+        Number of nearly constant level sidelobes adjacent to the mainlobe.
+    sll : float, optional
+        Desired suppression of sidelobe level in decibels (dB) relative to the
+        DC gain of the mainlobe. This should be a positive number.
+    norm : bool, optional
+        When True (default), divides the window by the largest (middle) value
+        for odd-length windows or the value that would occur between the two
+        repeated middle values for even-length windows such that all values
+        are less than or equal to 1. When False the DC gain will remain at 1
+        (0 dB) and the sidelobes will be `sll` dB down.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    out : array
+        The window. When `norm` is True (default), the maximum value is
+        normalized to 1 (though the value 1 does not appear if `M` is
+        even and `sym` is True).
+
+    See Also
+    --------
+    chebwin, kaiser, bartlett, blackman, hamming, hann
+
+    References
+    ----------
+    .. [1] W. Carrara, R. Goodman, and R. Majewski, "Spotlight Synthetic
+           Aperture Radar: Signal Processing Algorithms" Pages 512-513,
+           July 1995.
+    .. [2] Armin Doerry, "Catalog of Window Taper Functions for
+           Sidelobe Control", 2017.
+           https://www.researchgate.net/profile/Armin_Doerry/publication/316281181_Catalog_of_Window_Taper_Functions_for_Sidelobe_Control/links/58f92cb2a6fdccb121c9d54d/Catalog-of-Window-Taper-Functions-for-Sidelobe-Control.pdf
+
+    Examples
+    --------
+    Plot the window and its frequency response:
+
+    >>> import numpy as np
+    >>> from scipy import signal
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+
+    >>> window = signal.windows.taylor(51, nbar=20, sll=100, norm=False)
+    >>> plt.plot(window)
+    >>> plt.title("Taylor window (100 dB)")
+    >>> plt.ylabel("Amplitude")
+    >>> plt.xlabel("Sample")
+
+    >>> plt.figure()
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> plt.plot(freq, response)
+    >>> plt.axis([-0.5, 0.5, -120, 0])
+    >>> plt.title("Frequency response of the Taylor window (100 dB)")
+    >>> plt.ylabel("Normalized magnitude [dB]")
+    >>> plt.xlabel("Normalized frequency [cycles per sample]")
+
+    """  # noqa: E501
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    # Original text uses a negative sidelobe level parameter and then negates
+    # it in the calculation of B. To keep consistent with other methods we
+    # assume the sidelobe level parameter to be positive.
+    B = 10**(sll / 20)
+    A = np.arccosh(B) / np.pi
+    s2 = nbar**2 / (A**2 + (nbar - 0.5)**2)
+    ma = np.arange(1, nbar)
+
+    Fm = np.empty(nbar-1)
+    signs = np.empty_like(ma)
+    signs[::2] = 1
+    signs[1::2] = -1
+    m2 = ma*ma
+    for mi, m in enumerate(ma):
+        numer = signs[mi] * np.prod(1 - m2[mi]/s2/(A**2 + (ma - 0.5)**2))
+        denom = 2 * np.prod(1 - m2[mi]/m2[:mi]) * np.prod(1 - m2[mi]/m2[mi+1:])
+        Fm[mi] = numer / denom
+
+    def W(n):
+        return 1 + 2*np.dot(Fm, np.cos(
+            2*np.pi*ma[:, np.newaxis]*(n-M/2.+0.5)/M))
+
+    w = W(np.arange(M))
+
+    # normalize (Note that this is not described in the original text [1])
+    if norm:
+        scale = 1.0 / W((M - 1) / 2)
+        w *= scale
+
+    return _truncate(w, needs_trunc)
+
+
+def dpss(M, NW, Kmax=None, sym=True, norm=None, return_ratios=False):
+    """
+    Compute the Discrete Prolate Spheroidal Sequences (DPSS).
+
+    DPSS (or Slepian sequences) are often used in multitaper power spectral
+    density estimation (see [1]_). The first window in the sequence can be
+    used to maximize the energy concentration in the main lobe, and is also
+    called the Slepian window.
+
+    Parameters
+    ----------
+    M : int
+        Window length.
+    NW : float
+        Standardized half bandwidth corresponding to ``2*NW = BW/f0 = BW*M*dt``
+        where ``dt`` is taken as 1.
+    Kmax : int | None, optional
+        Number of DPSS windows to return (orders ``0`` through ``Kmax-1``).
+        If None (default), return only a single window of shape ``(M,)``
+        instead of an array of windows of shape ``(Kmax, M)``.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+    norm : {2, 'approximate', 'subsample'} | None, optional
+        If 'approximate' or 'subsample', then the windows are normalized by the
+        maximum, and a correction scale-factor for even-length windows
+        is applied either using ``M**2/(M**2+NW)`` ("approximate") or
+        a FFT-based subsample shift ("subsample"), see Notes for details.
+        If None, then "approximate" is used when ``Kmax=None`` and 2 otherwise
+        (which uses the l2 norm).
+    return_ratios : bool, optional
+        If True, also return the concentration ratios in addition to the
+        windows.
+
+    Returns
+    -------
+    v : ndarray, shape (Kmax, M) or (M,)
+        The DPSS windows. Will be 1D if `Kmax` is None.
+    r : ndarray, shape (Kmax,) or float, optional
+        The concentration ratios for the windows. Only returned if
+        `return_ratios` evaluates to True. Will be 0D if `Kmax` is None.
+
+    Notes
+    -----
+    This computation uses the tridiagonal eigenvector formulation given
+    in [2]_.
+
+    The default normalization for ``Kmax=None``, i.e. window-generation mode,
+    simply using the l-infinity norm would create a window with two unity
+    values, which creates slight normalization differences between even and odd
+    orders. The approximate correction of ``M**2/float(M**2+NW)`` for even
+    sample numbers is used to counteract this effect (see Examples below).
+
+    For very long signals (e.g., 1e6 elements), it can be useful to compute
+    windows orders of magnitude shorter and use interpolation (e.g.,
+    `scipy.interpolate.interp1d`) to obtain tapers of length `M`,
+    but this in general will not preserve orthogonality between the tapers.
+
+    .. versionadded:: 1.1
+
+    References
+    ----------
+    .. [1] Percival DB, Walden WT. Spectral Analysis for Physical Applications:
+       Multitaper and Conventional Univariate Techniques.
+       Cambridge University Press; 1993.
+    .. [2] Slepian, D. Prolate spheroidal wave functions, Fourier analysis, and
+       uncertainty V: The discrete case. Bell System Technical Journal,
+       Volume 57 (1978), 1371430.
+    .. [3] Kaiser, JF, Schafer RW. On the Use of the I0-Sinh Window for
+       Spectrum Analysis. IEEE Transactions on Acoustics, Speech and
+       Signal Processing. ASSP-28 (1): 105-107; 1980.
+
+    Examples
+    --------
+    We can compare the window to `kaiser`, which was invented as an alternative
+    that was easier to calculate [3]_ (example adapted from
+    `here `_):
+
+    >>> import numpy as np
+    >>> import matplotlib.pyplot as plt
+    >>> from scipy.signal import windows, freqz
+    >>> M = 51
+    >>> fig, axes = plt.subplots(3, 2, figsize=(5, 7))
+    >>> for ai, alpha in enumerate((1, 3, 5)):
+    ...     win_dpss = windows.dpss(M, alpha)
+    ...     beta = alpha*np.pi
+    ...     win_kaiser = windows.kaiser(M, beta)
+    ...     for win, c in ((win_dpss, 'k'), (win_kaiser, 'r')):
+    ...         win /= win.sum()
+    ...         axes[ai, 0].plot(win, color=c, lw=1.)
+    ...         axes[ai, 0].set(xlim=[0, M-1], title=r'$\\alpha$ = %s' % alpha,
+    ...                         ylabel='Amplitude')
+    ...         w, h = freqz(win)
+    ...         axes[ai, 1].plot(w, 20 * np.log10(np.abs(h)), color=c, lw=1.)
+    ...         axes[ai, 1].set(xlim=[0, np.pi],
+    ...                         title=r'$\\beta$ = %0.2f' % beta,
+    ...                         ylabel='Magnitude (dB)')
+    >>> for ax in axes.ravel():
+    ...     ax.grid(True)
+    >>> axes[2, 1].legend(['DPSS', 'Kaiser'])
+    >>> fig.tight_layout()
+    >>> plt.show()
+
+    And here are examples of the first four windows, along with their
+    concentration ratios:
+
+    >>> M = 512
+    >>> NW = 2.5
+    >>> win, eigvals = windows.dpss(M, NW, 4, return_ratios=True)
+    >>> fig, ax = plt.subplots(1)
+    >>> ax.plot(win.T, linewidth=1.)
+    >>> ax.set(xlim=[0, M-1], ylim=[-0.1, 0.1], xlabel='Samples',
+    ...        title='DPSS, M=%d, NW=%0.1f' % (M, NW))
+    >>> ax.legend(['win[%d] (%0.4f)' % (ii, ratio)
+    ...            for ii, ratio in enumerate(eigvals)])
+    >>> fig.tight_layout()
+    >>> plt.show()
+
+    Using a standard :math:`l_{\\infty}` norm would produce two unity values
+    for even `M`, but only one unity value for odd `M`. This produces uneven
+    window power that can be counteracted by the approximate correction
+    ``M**2/float(M**2+NW)``, which can be selected by using
+    ``norm='approximate'`` (which is the same as ``norm=None`` when
+    ``Kmax=None``, as is the case here). Alternatively, the slower
+    ``norm='subsample'`` can be used, which uses subsample shifting in the
+    frequency domain (FFT) to compute the correction:
+
+    >>> Ms = np.arange(1, 41)
+    >>> factors = (50, 20, 10, 5, 2.0001)
+    >>> energy = np.empty((3, len(Ms), len(factors)))
+    >>> for mi, M in enumerate(Ms):
+    ...     for fi, factor in enumerate(factors):
+    ...         NW = M / float(factor)
+    ...         # Corrected using empirical approximation (default)
+    ...         win = windows.dpss(M, NW)
+    ...         energy[0, mi, fi] = np.sum(win ** 2) / np.sqrt(M)
+    ...         # Corrected using subsample shifting
+    ...         win = windows.dpss(M, NW, norm='subsample')
+    ...         energy[1, mi, fi] = np.sum(win ** 2) / np.sqrt(M)
+    ...         # Uncorrected (using l-infinity norm)
+    ...         win /= win.max()
+    ...         energy[2, mi, fi] = np.sum(win ** 2) / np.sqrt(M)
+    >>> fig, ax = plt.subplots(1)
+    >>> hs = ax.plot(Ms, energy[2], '-o', markersize=4,
+    ...              markeredgecolor='none')
+    >>> leg = [hs[-1]]
+    >>> for hi, hh in enumerate(hs):
+    ...     h1 = ax.plot(Ms, energy[0, :, hi], '-o', markersize=4,
+    ...                  color=hh.get_color(), markeredgecolor='none',
+    ...                  alpha=0.66)
+    ...     h2 = ax.plot(Ms, energy[1, :, hi], '-o', markersize=4,
+    ...                  color=hh.get_color(), markeredgecolor='none',
+    ...                  alpha=0.33)
+    ...     if hi == len(hs) - 1:
+    ...         leg.insert(0, h1[0])
+    ...         leg.insert(0, h2[0])
+    >>> ax.set(xlabel='M (samples)', ylabel=r'Power / $\\sqrt{M}$')
+    >>> ax.legend(leg, ['Uncorrected', r'Corrected: $\\frac{M^2}{M^2+NW}$',
+    ...                 'Corrected (subsample)'])
+    >>> fig.tight_layout()
+
+    """
+    if _len_guards(M):
+        return np.ones(M)
+    if norm is None:
+        norm = 'approximate' if Kmax is None else 2
+    known_norms = (2, 'approximate', 'subsample')
+    if norm not in known_norms:
+        raise ValueError(f'norm must be one of {known_norms}, got {norm}')
+    if Kmax is None:
+        singleton = True
+        Kmax = 1
+    else:
+        singleton = False
+    Kmax = operator.index(Kmax)
+    if not 0 < Kmax <= M:
+        raise ValueError('Kmax must be greater than 0 and less than M')
+    if NW >= M/2.:
+        raise ValueError('NW must be less than M/2.')
+    if NW <= 0:
+        raise ValueError('NW must be positive')
+    M, needs_trunc = _extend(M, sym)
+    W = float(NW) / M
+    nidx = np.arange(M)
+
+    # Here we want to set up an optimization problem to find a sequence
+    # whose energy is maximally concentrated within band [-W,W].
+    # Thus, the measure lambda(T,W) is the ratio between the energy within
+    # that band, and the total energy. This leads to the eigen-system
+    # (A - (l1)I)v = 0, where the eigenvector corresponding to the largest
+    # eigenvalue is the sequence with maximally concentrated energy. The
+    # collection of eigenvectors of this system are called Slepian
+    # sequences, or discrete prolate spheroidal sequences (DPSS). Only the
+    # first K, K = 2NW/dt orders of DPSS will exhibit good spectral
+    # concentration
+    # [see https://en.wikipedia.org/wiki/Spectral_concentration_problem]
+
+    # Here we set up an alternative symmetric tri-diagonal eigenvalue
+    # problem such that
+    # (B - (l2)I)v = 0, and v are our DPSS (but eigenvalues l2 != l1)
+    # the main diagonal = ([M-1-2*t]/2)**2 cos(2PIW), t=[0,1,2,...,M-1]
+    # and the first off-diagonal = t(M-t)/2, t=[1,2,...,M-1]
+    # [see Percival and Walden, 1993]
+    d = ((M - 1 - 2 * nidx) / 2.) ** 2 * np.cos(2 * np.pi * W)
+    e = nidx[1:] * (M - nidx[1:]) / 2.
+
+    # only calculate the highest Kmax eigenvalues
+    w, windows = linalg.eigh_tridiagonal(
+        d, e, select='i', select_range=(M - Kmax, M - 1))
+    w = w[::-1]
+    windows = windows[:, ::-1].T
+
+    # By convention (Percival and Walden, 1993 pg 379)
+    # * symmetric tapers (k=0,2,4,...) should have a positive average.
+    fix_even = (windows[::2].sum(axis=1) < 0)
+    for i, f in enumerate(fix_even):
+        if f:
+            windows[2 * i] *= -1
+    # * antisymmetric tapers should begin with a positive lobe
+    #   (this depends on the definition of "lobe", here we'll take the first
+    #   point above the numerical noise, which should be good enough for
+    #   sufficiently smooth functions, and more robust than relying on an
+    #   algorithm that uses max(abs(w)), which is susceptible to numerical
+    #   noise problems)
+    thresh = max(1e-7, 1. / M)
+    for i, w in enumerate(windows[1::2]):
+        if w[w * w > thresh][0] < 0:
+            windows[2 * i + 1] *= -1
+
+    # Now find the eigenvalues of the original spectral concentration problem
+    # Use the autocorr sequence technique from Percival and Walden, 1993 pg 390
+    if return_ratios:
+        dpss_rxx = _fftautocorr(windows)
+        r = 4 * W * np.sinc(2 * W * nidx)
+        r[0] = 2 * W
+        ratios = np.dot(dpss_rxx, r)
+        if singleton:
+            ratios = ratios[0]
+    # Deal with sym and Kmax=None
+    if norm != 2:
+        windows /= windows.max()
+        if M % 2 == 0:
+            if norm == 'approximate':
+                correction = M**2 / float(M**2 + NW)
+            else:
+                s = sp_fft.rfft(windows[0])
+                shift = -(1 - 1./M) * np.arange(1, M//2 + 1)
+                s[1:] *= 2 * np.exp(-1j * np.pi * shift)
+                correction = M / s.real.sum()
+            windows *= correction
+    # else we're already l2 normed, so do nothing
+    if needs_trunc:
+        windows = windows[:, :-1]
+    if singleton:
+        windows = windows[0]
+    return (windows, ratios) if return_ratios else windows
+
+
+def lanczos(M, *, sym=True):
+    r"""Return a Lanczos window also known as a sinc window.
+
+    Parameters
+    ----------
+    M : int
+        Number of points in the output window. If zero, an empty array
+        is returned. An exception is thrown when it is negative.
+    sym : bool, optional
+        When True (default), generates a symmetric window, for use in filter
+        design.
+        When False, generates a periodic window, for use in spectral analysis.
+
+    Returns
+    -------
+    w : ndarray
+        The window, with the maximum value normalized to 1 (though the value 1
+        does not appear if `M` is even and `sym` is True).
+
+    Notes
+    -----
+    The Lanczos window is defined as
+
+    .. math::  w(n) = sinc \left( \frac{2n}{M - 1} - 1 \right)
+
+    where
+
+    .. math::  sinc(x) = \frac{\sin(\pi x)}{\pi x}
+
+    The Lanczos window has reduced Gibbs oscillations and is widely used for
+    filtering climate timeseries with good properties in the physical and
+    spectral domains.
+
+    .. versionadded:: 1.10
+
+    References
+    ----------
+    .. [1] Lanczos, C., and Teichmann, T. (1957). Applied analysis.
+           Physics Today, 10, 44.
+    .. [2] Duchon C. E. (1979) Lanczos Filtering in One and Two Dimensions.
+           Journal of Applied Meteorology, Vol 18, pp 1016-1022.
+    .. [3] Thomson, R. E. and Emery, W. J. (2014) Data Analysis Methods in
+           Physical Oceanography (Third Edition), Elsevier, pp 593-637.
+    .. [4] Wikipedia, "Window function",
+           http://en.wikipedia.org/wiki/Window_function
+
+    Examples
+    --------
+    Plot the window
+
+    >>> import numpy as np
+    >>> from scipy.signal.windows import lanczos
+    >>> from scipy.fft import fft, fftshift
+    >>> import matplotlib.pyplot as plt
+    >>> fig, ax = plt.subplots(1)
+    >>> window = lanczos(51)
+    >>> ax.plot(window)
+    >>> ax.set_title("Lanczos window")
+    >>> ax.set_ylabel("Amplitude")
+    >>> ax.set_xlabel("Sample")
+    >>> fig.tight_layout()
+    >>> plt.show()
+
+    and its frequency response:
+
+    >>> fig, ax = plt.subplots(1)
+    >>> A = fft(window, 2048) / (len(window)/2.0)
+    >>> freq = np.linspace(-0.5, 0.5, len(A))
+    >>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
+    >>> ax.plot(freq, response)
+    >>> ax.set_xlim(-0.5, 0.5)
+    >>> ax.set_ylim(-120, 0)
+    >>> ax.set_title("Frequency response of the lanczos window")
+    >>> ax.set_ylabel("Normalized magnitude [dB]")
+    >>> ax.set_xlabel("Normalized frequency [cycles per sample]")
+    >>> fig.tight_layout()
+    >>> plt.show()
+    """
+    if _len_guards(M):
+        return np.ones(M)
+    M, needs_trunc = _extend(M, sym)
+
+    # To make sure that the window is symmetric, we concatenate the right hand
+    # half of the window and the flipped one which is the left hand half of
+    # the window.
+    def _calc_right_side_lanczos(n, m):
+        return np.sinc(2. * np.arange(n, m) / (m - 1) - 1.0)
+
+    if M % 2 == 0:
+        wh = _calc_right_side_lanczos(M/2, M)
+        w = np.r_[np.flip(wh), wh]
+    else:
+        wh = _calc_right_side_lanczos((M+1)/2, M)
+        w = np.r_[np.flip(wh), 1.0, wh]
+
+    return _truncate(w, needs_trunc)
+
+
+def _fftautocorr(x):
+    """Compute the autocorrelation of a real array and crop the result."""
+    N = x.shape[-1]
+    use_N = sp_fft.next_fast_len(2*N-1)
+    x_fft = sp_fft.rfft(x, use_N, axis=-1)
+    cxy = sp_fft.irfft(x_fft * x_fft.conj(), n=use_N)[:, :N]
+    # Or equivalently (but in most cases slower):
+    # cxy = np.array([np.convolve(xx, yy[::-1], mode='full')
+    #                 for xx, yy in zip(x, x)])[:, N-1:2*N-1]
+    return cxy
+
+
+_win_equiv_raw = {
+    ('barthann', 'brthan', 'bth'): (barthann, False),
+    ('bartlett', 'bart', 'brt'): (bartlett, False),
+    ('blackman', 'black', 'blk'): (blackman, False),
+    ('blackmanharris', 'blackharr', 'bkh'): (blackmanharris, False),
+    ('bohman', 'bman', 'bmn'): (bohman, False),
+    ('boxcar', 'box', 'ones',
+        'rect', 'rectangular'): (boxcar, False),
+    ('chebwin', 'cheb'): (chebwin, True),
+    ('cosine', 'halfcosine'): (cosine, False),
+    ('dpss',): (dpss, True),
+    ('exponential', 'poisson'): (exponential, False),
+    ('flattop', 'flat', 'flt'): (flattop, False),
+    ('gaussian', 'gauss', 'gss'): (gaussian, True),
+    ('general cosine', 'general_cosine'): (general_cosine, True),
+    ('general gaussian', 'general_gaussian',
+        'general gauss', 'general_gauss', 'ggs'): (general_gaussian, True),
+    ('general hamming', 'general_hamming'): (general_hamming, True),
+    ('hamming', 'hamm', 'ham'): (hamming, False),
+    ('hann', 'han'): (hann, False),
+    ('kaiser', 'ksr'): (kaiser, True),
+    ('kaiser bessel derived', 'kbd'): (kaiser_bessel_derived, True),
+    ('lanczos', 'sinc'): (lanczos, False),
+    ('nuttall', 'nutl', 'nut'): (nuttall, False),
+    ('parzen', 'parz', 'par'): (parzen, False),
+    ('taylor', 'taylorwin'): (taylor, False),
+    ('triangle', 'triang', 'tri'): (triang, False),
+    ('tukey', 'tuk'): (tukey, False),
+}
+
+# Fill dict with all valid window name strings
+_win_equiv = {}
+for k, v in _win_equiv_raw.items():
+    for key in k:
+        _win_equiv[key] = v[0]
+
+# Keep track of which windows need additional parameters
+_needs_param = set()
+for k, v in _win_equiv_raw.items():
+    if v[1]:
+        _needs_param.update(k)
+
+
+def get_window(window, Nx, fftbins=True):
+    """
+    Return a window of a given length and type.
+
+    Parameters
+    ----------
+    window : string, float, or tuple
+        The type of window to create. See below for more details.
+    Nx : int
+        The number of samples in the window.
+    fftbins : bool, optional
+        If True (default), create a "periodic" window, ready to use with
+        `ifftshift` and be multiplied by the result of an FFT (see also
+        :func:`~scipy.fft.fftfreq`).
+        If False, create a "symmetric" window, for use in filter design.
+
+    Returns
+    -------
+    get_window : ndarray
+        Returns a window of length `Nx` and type `window`
+
+    Notes
+    -----
+    Window types:
+
+    - `~scipy.signal.windows.boxcar`
+    - `~scipy.signal.windows.triang`
+    - `~scipy.signal.windows.blackman`
+    - `~scipy.signal.windows.hamming`
+    - `~scipy.signal.windows.hann`
+    - `~scipy.signal.windows.bartlett`
+    - `~scipy.signal.windows.flattop`
+    - `~scipy.signal.windows.parzen`
+    - `~scipy.signal.windows.bohman`
+    - `~scipy.signal.windows.blackmanharris`
+    - `~scipy.signal.windows.nuttall`
+    - `~scipy.signal.windows.barthann`
+    - `~scipy.signal.windows.cosine`
+    - `~scipy.signal.windows.exponential`
+    - `~scipy.signal.windows.tukey`
+    - `~scipy.signal.windows.taylor`
+    - `~scipy.signal.windows.lanczos`
+    - `~scipy.signal.windows.kaiser` (needs beta)
+    - `~scipy.signal.windows.kaiser_bessel_derived` (needs beta)
+    - `~scipy.signal.windows.gaussian` (needs standard deviation)
+    - `~scipy.signal.windows.general_cosine` (needs weighting coefficients)
+    - `~scipy.signal.windows.general_gaussian` (needs power, width)
+    - `~scipy.signal.windows.general_hamming` (needs window coefficient)
+    - `~scipy.signal.windows.dpss` (needs normalized half-bandwidth)
+    - `~scipy.signal.windows.chebwin` (needs attenuation)
+
+
+    If the window requires no parameters, then `window` can be a string.
+
+    If the window requires parameters, then `window` must be a tuple
+    with the first argument the string name of the window, and the next
+    arguments the needed parameters.
+
+    If `window` is a floating point number, it is interpreted as the beta
+    parameter of the `~scipy.signal.windows.kaiser` window.
+
+    Each of the window types listed above is also the name of
+    a function that can be called directly to create a window of
+    that type.
+
+    Examples
+    --------
+    >>> from scipy import signal
+    >>> signal.get_window('triang', 7)
+    array([ 0.125,  0.375,  0.625,  0.875,  0.875,  0.625,  0.375])
+    >>> signal.get_window(('kaiser', 4.0), 9)
+    array([ 0.08848053,  0.29425961,  0.56437221,  0.82160913,  0.97885093,
+            0.97885093,  0.82160913,  0.56437221,  0.29425961])
+    >>> signal.get_window(('exponential', None, 1.), 9)
+    array([ 0.011109  ,  0.03019738,  0.082085  ,  0.22313016,  0.60653066,
+            0.60653066,  0.22313016,  0.082085  ,  0.03019738])
+    >>> signal.get_window(4.0, 9)
+    array([ 0.08848053,  0.29425961,  0.56437221,  0.82160913,  0.97885093,
+            0.97885093,  0.82160913,  0.56437221,  0.29425961])
+
+    """
+    sym = not fftbins
+    try:
+        beta = float(window)
+    except (TypeError, ValueError) as e:
+        args = ()
+        if isinstance(window, tuple):
+            winstr = window[0]
+            if len(window) > 1:
+                args = window[1:]
+        elif isinstance(window, str):
+            if window in _needs_param:
+                raise ValueError("The '" + window + "' window needs one or "
+                                 "more parameters -- pass a tuple.") from e
+            else:
+                winstr = window
+        else:
+            raise ValueError("%s as window type is not supported." %
+                             str(type(window))) from e
+
+        try:
+            winfunc = _win_equiv[winstr]
+        except KeyError as e:
+            raise ValueError("Unknown window type.") from e
+
+        if winfunc is dpss:
+            params = (Nx,) + args + (None,)
+        else:
+            params = (Nx,) + args
+    else:
+        winfunc = kaiser
+        params = (Nx, beta)
+
+    return winfunc(*params, sym=sym)
diff --git a/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/windows.py b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/windows.py
new file mode 100644
index 0000000000000000000000000000000000000000..6858f71aceeb29ca6110864d01fb250e8c8ce403
--- /dev/null
+++ b/emissary-ml/llm-scripts/fine-tuning/llama3/venv/lib/python3.10/site-packages/scipy/signal/windows/windows.py
@@ -0,0 +1,23 @@
+# This file is not meant for public use and will be removed in SciPy v2.0.0.
+# Use the `scipy.signal.windows` namespace for importing the functions
+# included below.
+
+from scipy._lib.deprecation import _sub_module_deprecation
+
+__all__ = [  # noqa: F822
+    'boxcar', 'triang', 'parzen', 'bohman', 'blackman', 'nuttall',
+    'blackmanharris', 'flattop', 'bartlett', 'barthann',
+    'hamming', 'kaiser', 'gaussian', 'general_cosine',
+    'general_gaussian', 'general_hamming', 'chebwin', 'cosine',
+    'hann', 'exponential', 'tukey', 'taylor', 'dpss', 'get_window',
+]
+
+
+def __dir__():
+    return __all__
+
+
+def __getattr__(name):
+    return _sub_module_deprecation(sub_package="signal.windows", module="windows",
+                                   private_modules=["_windows"], all=__all__,
+                                   attribute=name)